From 81eae816832843a90299062b00339c922a6b15c2 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Fri, 6 Dec 2024 17:09:26 +0100 Subject: [PATCH 1/7] Add partial role support for manager only - Add the custom role which replaces the manager role - Added mini-details endpoint used by v2024.11.1 These changes try to add the custom role in such a way that it stays compatible with the older manager role. It will convert a manager role into a custom role, and if a manager has `access-all` rights, it will enable the correct custom roles. Upon saving it will convert these back to the old format. What this does is making sure you are able to revert back to an older version of Vaultwarden without issues. This way we can support newer web-vault's and still be compatible with a previous Vaultwarden version if needed. In the future this needs to be changed to full role support though. Fixed the 2FA hide CSS since the order of options has changed Signed-off-by: BlackDex --- src/api/core/organizations.rs | 114 +++++++++++++-- src/api/web.rs | 39 +---- src/config.rs | 43 +++++- src/db/models/group.rs | 5 +- src/db/models/organization.rs | 136 +++++++++++++----- .../templates/scss/vaultwarden.scss.hbs | 34 +++-- 6 files changed, 277 insertions(+), 94 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index ce9b7921d5..2551315981 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -48,6 +48,7 @@ pub fn routes() -> Vec { confirm_invite, bulk_confirm_invite, accept_invite, + get_org_user_mini_details, get_user, edit_user, put_organization_user, @@ -323,6 +324,13 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, // get all collection memberships for the current organization let coll_users = CollectionUser::find_by_organization(org_id, &mut conn).await; + // Generate a HashMap to get the correct UserOrgType per user to determine the manage permission + // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser + let users_org_type: HashMap = UserOrganization::find_confirmed_by_org(org_id, &mut conn) + .await + .into_iter() + .map(|uo| (uo.uuid, uo.atype)) + .collect(); // check if current user has full access to the organization (either directly or via any group) let has_full_access_to_org = user_org.access_all @@ -336,11 +344,22 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, || (CONFIG.org_groups_enabled() && GroupUser::has_access_to_collection_by_member(&col.uuid, &user_org.uuid, &mut conn).await); + // Not assigned collections should not be returned + if !assigned { + continue; + } + // get the users assigned directly to the given collection let users: Vec = coll_users .iter() .filter(|collection_user| collection_user.collection_uuid == col.uuid) - .map(|collection_user| SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()) + .map(|collection_user| { + SelectionReadOnly::to_collection_user_details_read_only( + collection_user, + *users_org_type.get(&collection_user.user_uuid).unwrap_or(&(UserOrgType::User as i32)), + ) + .to_json() + }) .collect(); // get the group details for the given collection @@ -645,12 +664,24 @@ async fn get_org_collection_detail( Vec::with_capacity(0) }; + // Generate a HashMap to get the correct UserOrgType per user to determine the manage permission + // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser + let users_org_type: HashMap = UserOrganization::find_confirmed_by_org(org_id, &mut conn) + .await + .into_iter() + .map(|uo| (uo.uuid, uo.atype)) + .collect(); + let users: Vec = CollectionUser::find_by_collection_swap_user_uuid_with_org_user_uuid(&collection.uuid, &mut conn) .await .iter() .map(|collection_user| { - SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json() + SelectionReadOnly::to_collection_user_details_read_only( + collection_user, + *users_org_type.get(&collection_user.user_uuid).unwrap_or(&(UserOrgType::User as i32)), + ) + .to_json() }) .collect(); @@ -830,13 +861,19 @@ struct InviteData { collections: Option>, #[serde(default)] access_all: bool, + #[serde(default)] + permissions: HashMap, } #[post("/organizations//users/invite", data = "")] async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { - let data: InviteData = data.into_inner(); + let mut data: InviteData = data.into_inner(); - let new_type = match UserOrgType::from_str(&data.r#type.into_string()) { + // HACK: We need the raw user-type be be sure custom role is selected to determine the access_all permission + // The from_str() will convert the custom role type into a manager role type + let raw_type = &data.r#type.into_string(); + // UserOrgType::from_str will convert custom (4) to manager (3) + let new_type = match UserOrgType::from_str(raw_type) { Some(new_type) => new_type as i32, None => err!("Invalid type"), }; @@ -845,6 +882,17 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders err!("Only Owners can invite Managers, Admins or Owners") } + // HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag + // Since the parent checkbox is not send to the server we need to check and verify the child checkboxes + // If the box is not checked, the user will still be a manager, but not with the access_all permission + if raw_type.eq("4") + && data.permissions.get("editAnyCollection") == Some(&json!(true)) + && data.permissions.get("deleteAnyCollection") == Some(&json!(true)) + && data.permissions.get("createNewCollections") == Some(&json!(true)) + { + data.access_all = true; + } + for email in data.emails.iter() { let mut user_org_status = UserOrgStatus::Invited as i32; let user = match User::find_by_mail(email, &mut conn).await { @@ -1254,7 +1302,21 @@ async fn _confirm_invite( save_result } -#[get("/organizations//users/?")] +#[get("/organizations//users/mini-details", rank = 1)] +async fn get_org_user_mini_details(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { + let mut users_json = Vec::new(); + for u in UserOrganization::find_by_org(org_id, &mut conn).await { + users_json.push(u.to_json_mini_details(&mut conn).await); + } + + Json(json!({ + "data": users_json, + "object": "list", + "continuationToken": null, + })) +} + +#[get("/organizations//users/?", rank = 2)] async fn get_user( org_id: &str, org_user_id: &str, @@ -1282,6 +1344,8 @@ struct EditUserData { groups: Option>, #[serde(default)] access_all: bool, + #[serde(default)] + permissions: HashMap, } #[put("/organizations//users/", data = "", rank = 1)] @@ -1303,14 +1367,30 @@ async fn edit_user( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { - let data: EditUserData = data.into_inner(); + let mut data: EditUserData = data.into_inner(); - let Some(new_type) = UserOrgType::from_str(&data.r#type.into_string()) else { + // HACK: We need the raw user-type be be sure custom role is selected to determine the access_all permission + // The from_str() will convert the custom role type into a manager role type + let raw_type = &data.r#type.into_string(); + // UserOrgType::from_str will convert custom (4) to manager (3) + let Some(new_type) = UserOrgType::from_str(raw_type) else { err!("Invalid type") }; - let Some(mut user_to_edit) = UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await else { - err!("The specified user isn't member of the organization") + // HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag + // Since the parent checkbox is not send to the server we need to check and verify the child checkboxes + // If the box is not checked, the user will still be a manager, but not with the access_all permission + if raw_type.eq("4") + && data.permissions.get("editAnyCollection") == Some(&json!(true)) + && data.permissions.get("deleteAnyCollection") == Some(&json!(true)) + && data.permissions.get("createNewCollections") == Some(&json!(true)) + { + data.access_all = true; + } + + let mut user_to_edit = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, &mut conn).await { + Some(user) => user, + None => err!("The specified user isn't member of the organization"), }; if new_type != user_to_edit.atype @@ -2331,6 +2411,7 @@ struct SelectionReadOnly { id: String, read_only: bool, hide_passwords: bool, + manage: bool, } impl SelectionReadOnly { @@ -2339,18 +2420,31 @@ impl SelectionReadOnly { } pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly { + // If both read_only and hide_passwords are false, then manage should be true + // You can't have an entry with read_only and manage, or hide_passwords and manage + // Or an entry with everything to false SelectionReadOnly { id: collection_group.groups_uuid.clone(), read_only: collection_group.read_only, hide_passwords: collection_group.hide_passwords, + manage: !collection_group.read_only && !collection_group.hide_passwords, } } - pub fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> SelectionReadOnly { + pub fn to_collection_user_details_read_only( + collection_user: &CollectionUser, + user_org_type: i32, + ) -> SelectionReadOnly { + // Vaultwarden allows manage access for Admins and Owners by default + // For managers (Or custom role) it depends if they have read_ony or hide_passwords set to true or not SelectionReadOnly { id: collection_user.user_uuid.clone(), read_only: collection_user.read_only, hide_passwords: collection_user.hide_passwords, + manage: user_org_type >= UserOrgType::Admin + || (user_org_type == UserOrgType::Manager + && !collection_user.read_only + && !collection_user.hide_passwords), } } diff --git a/src/api/web.rs b/src/api/web.rs index a96d7e2a16..edbffbbddb 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -1,4 +1,3 @@ -use once_cell::sync::Lazy; use std::path::{Path, PathBuf}; use rocket::{ @@ -14,7 +13,7 @@ use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::decode_file_download, error::Error, - util::{get_web_vault_version, Cached, SafeString}, + util::{Cached, SafeString}, CONFIG, }; @@ -54,43 +53,7 @@ fn not_found() -> ApiResult> { #[get("/css/vaultwarden.css")] fn vaultwarden_css() -> Cached> { - // 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 = 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::().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 = 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::().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()), diff --git a/src/config.rs b/src/config.rs index ab62a020ea..e8536209d8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,7 +12,7 @@ use reqwest::Url; use crate::{ db::DbConnType, error::Error, - util::{get_env, get_env_bool, parse_experimental_client_feature_flags}, + util::{get_env, get_env_bool, get_web_vault_version, parse_experimental_client_feature_flags}, }; static CONFIG_FILE: Lazy = Lazy::new(|| { @@ -1327,6 +1327,8 @@ where // Register helpers hb.register_helper("case", Box::new(case_helper)); hb.register_helper("to_json", Box::new(to_json)); + hb.register_helper("webver", Box::new(webver)); + hb.register_helper("vwver", Box::new(vwver)); macro_rules! reg { ($name:expr) => {{ @@ -1430,3 +1432,42 @@ fn to_json<'reg, 'rc>( out.write(&json)?; Ok(()) } + +// 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 = Lazy::new(|| { + let vault_version = get_web_vault_version(); + // Use a single regex capture to extract version components + let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap(); + re.captures(&vault_version) + .and_then(|c| { + (c.len() == 4).then(|| { + format!("{}.{}.{}", c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str(), c.get(3).unwrap().as_str()) + }) + }) + .and_then(|v| semver::Version::parse(&v).ok()) + .unwrap_or_else(|| semver::Version::parse("2024.6.2").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 = Lazy::new(|| { + let vw_version = crate::VERSION.unwrap_or("1.32.5"); + // Use a single regex capture to extract version components + let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap(); + re.captures(vw_version) + .and_then(|c| { + (c.len() == 4).then(|| { + format!("{}.{}.{}", c.get(1).unwrap().as_str(), c.get(2).unwrap().as_str(), c.get(3).unwrap().as_str()) + }) + }) + .and_then(|v| semver::Version::parse(&v).ok()) + .unwrap_or_else(|| semver::Version::parse("1.32.5").unwrap()) +}); + +handlebars::handlebars_helper!(webver: | web_vault_version: String | + semver::VersionReq::parse(&web_vault_version).expect("Invalid web-vault version compare string").matches(&WEB_VAULT_VERSION) +); +handlebars::handlebars_helper!(vwver: | vw_version: String | + semver::VersionReq::parse(&vw_version).expect("Invalid Vaultwarden version compare string").matches(&VW_VERSION) +); diff --git a/src/db/models/group.rs b/src/db/models/group.rs index d9a08970b6..84c2727a2d 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -74,6 +74,9 @@ impl Group { } pub async fn to_json_details(&self, conn: &mut DbConn) -> Value { + // If both read_only and hide_passwords are false, then manage should be true + // You can't have an entry with read_only and manage, or hide_passwords and manage + // Or an entry with everything to false let collections_groups: Vec = CollectionGroup::find_by_group(&self.uuid, conn) .await .iter() @@ -82,7 +85,7 @@ impl Group { "id": entry.collections_uuid, "readOnly": entry.read_only, "hidePasswords": entry.hide_passwords, - "manage": false + "manage": !entry.read_only && !entry.hide_passwords, }) }) .collect(); diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 15f009918f..5f8434dea9 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -73,6 +73,8 @@ impl UserOrgType { "1" | "Admin" => Some(UserOrgType::Admin), "2" | "User" => Some(UserOrgType::User), "3" | "Manager" => Some(UserOrgType::Manager), + // HACK: We convert the custom role to a manager role + "4" | "Custom" => Some(UserOrgType::Manager), _ => None, } } @@ -85,7 +87,7 @@ impl Ord for UserOrgType { 3, // Owner 2, // Admin 0, // User - 1, // Manager + 1, // Manager && Custom ]; ACCESS_LEVEL[*self as usize].cmp(&ACCESS_LEVEL[*other as usize]) } @@ -158,33 +160,46 @@ impl Organization { pub fn to_json(&self) -> Value { json!({ "id": self.uuid, - "identifier": null, // not supported by us "name": self.name, "seats": null, "maxCollections": null, "maxStorageGb": i16::MAX, // The value doesn't matter, we don't check server-side "use2fa": true, - "useCustomPermissions": false, + "useCustomPermissions": true, "useDirectory": false, // Is supported, but this value isn't checked anywhere (yet) "useEvents": CONFIG.org_events_enabled(), "useGroups": CONFIG.org_groups_enabled(), "useTotp": true, "usePolicies": true, - // "useScim": false, // Not supported (Not AGPLv3 Licensed) + "useScim": false, // Not supported (Not AGPLv3 Licensed) "useSso": false, // Not supported - // "useKeyConnector": false, // Not supported + "useKeyConnector": false, // Not supported + "usePasswordManager": true, + "useSecretsManager": false, // Not supported (Not AGPLv3 Licensed) "selfHost": true, "useApi": true, "hasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), "useResetPassword": CONFIG.mail_enabled(), + "allowAdminAccessToAllCollectionItems": true, + "limitCollectionCreation": true, + "limitCollectionCreationDeletion": true, + "limitCollectionDeletion": true, - "businessName": null, + "businessName": self.name, "businessAddress1": null, "businessAddress2": null, "businessAddress3": null, "businessCountry": null, "businessTaxNumber": null, + "maxAutoscaleSeats": null, + "maxAutoscaleSmSeats": null, + "maxAutoscaleSmServiceAccounts": null, + + "secretsManagerPlan": null, + "smSeats": null, + "smServiceAccounts": null, + "billingEmail": self.billing_email, "planType": 6, // Custom plan "usersGetPremium": true, @@ -252,6 +267,15 @@ impl UserOrganization { } false } + + /// HACK: Convert the manager type to a custom type + /// It will be converted back on other locations + pub fn type_manager_as_custom(&self) -> i32 { + match self.atype { + 3 => 4, + _ => self.atype, + } + } } impl OrganizationApiKey { @@ -356,17 +380,21 @@ impl UserOrganization { pub async fn to_json(&self, conn: &mut DbConn) -> Value { let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap(); + // HACK: Convert the manager type to a custom type + // It will be converted back on other locations + let user_org_type = self.type_manager_as_custom(); + let permissions = json!({ - // TODO: Add support for Custom User Roles + // TODO: Add full support for Custom User Roles // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role + // Currently we use the custom role as a manager role and link the 3 Collection roles to mimic the access_all permission "accessEventLogs": false, "accessImportExport": false, "accessReports": false, - "createNewCollections": false, - "editAnyCollection": false, - "deleteAnyCollection": false, - "editAssignedCollections": false, - "deleteAssignedCollections": false, + // If the following 3 Collection roles are set to true a custom user has access all permission + "createNewCollections": user_org_type == 4 && self.access_all, + "editAnyCollection": user_org_type == 4 && self.access_all, + "deleteAnyCollection": user_org_type == 4 && self.access_all, "manageGroups": false, "managePolicies": false, "manageSso": false, // Not supported @@ -398,9 +426,9 @@ impl UserOrganization { "ssoBound": false, // Not supported "useSso": false, // Not supported "useKeyConnector": false, - "useSecretsManager": false, + "useSecretsManager": false, // Not supported (Not AGPLv3 Licensed) "usePasswordManager": true, - "useCustomPermissions": false, + "useCustomPermissions": true, "useActivateAutofillPolicy": false, "organizationUserId": self.uuid, @@ -417,9 +445,11 @@ impl UserOrganization { "familySponsorshipValidUntil": null, "familySponsorshipToDelete": null, "accessSecretsManager": false, - "limitCollectionCreationDeletion": false, // This should be set to true only when we can handle roles like createNewCollections + "limitCollectionCreation": true, + "limitCollectionCreationDeletion": true, + "limitCollectionDeletion": true, "allowAdminAccessToAllCollectionItems": true, - "flexibleCollections": false, + "userIsManagedByOrganization": false, // Means not managed via the Members UI, like SSO "permissions": permissions, @@ -429,7 +459,7 @@ impl UserOrganization { "userId": self.user_uuid, "key": self.akey, "status": self.status, - "type": self.atype, + "type": user_org_type, "enabled": true, "object": "profileOrganization", @@ -516,24 +546,34 @@ impl UserOrganization { Vec::with_capacity(0) }; - let permissions = json!({ - // TODO: Add support for Custom User Roles - // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role - "accessEventLogs": false, - "accessImportExport": false, - "accessReports": false, - "createNewCollections": false, - "editAnyCollection": false, - "deleteAnyCollection": false, - "editAssignedCollections": false, - "deleteAssignedCollections": false, - "manageGroups": false, - "managePolicies": false, - "manageSso": false, // Not supported - "manageUsers": false, - "manageResetPassword": false, - "manageScim": false // Not supported (Not AGPLv3 Licensed) - }); + // HACK: Convert the manager type to a custom type + // It will be converted back on other locations + let user_org_type = self.type_manager_as_custom(); + + // HACK: Only return permissions if the user is of type custom and has access_all + // Else Bitwarden will assume the defaults of all false + let permissions = if user_org_type == 4 && self.access_all { + json!({ + // TODO: Add full support for Custom User Roles + // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role + // Currently we use the custom role as a manager role and link the 3 Collection roles to mimic the access_all permission + "accessEventLogs": false, + "accessImportExport": false, + "accessReports": false, + // If the following 3 Collection roles are set to true a custom user has access all permission + "createNewCollections": true, + "editAnyCollection": true, + "deleteAnyCollection": true, + "manageGroups": false, + "managePolicies": false, + "manageSso": false, // Not supported + "manageUsers": false, + "manageResetPassword": false, + "manageScim": false // Not supported (Not AGPLv3 Licensed) + }) + } else { + json!(null) + }; json!({ "id": self.uuid, @@ -546,7 +586,7 @@ impl UserOrganization { "collections": collections, "status": status, - "type": self.atype, + "type": user_org_type, "accessAll": self.access_all, "twoFactorEnabled": twofactor_enabled, "resetPasswordEnrolled": self.reset_password_key.is_some(), @@ -608,6 +648,29 @@ impl UserOrganization { "object": "organizationUserDetails", }) } + + pub async fn to_json_mini_details(&self, conn: &mut DbConn) -> Value { + let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap(); + + // Because BitWarden want the status to be -1 for revoked users we need to catch that here. + // We subtract/add a number so we can restore/activate the user to it's previous state again. + let status = if self.status < UserOrgStatus::Revoked as i32 { + UserOrgStatus::Revoked as i32 + } else { + self.status + }; + + json!({ + "id": self.uuid, + "userId": self.user_uuid, + "type": self.type_manager_as_custom(), // HACK: Convert the manager type to a custom type + "status": status, + "name": user.name, + "email": user.email, + "object": "organizationUserUserMiniDetails", + }) + } + pub async fn save(&self, conn: &mut DbConn) -> EmptyResult { User::update_uuid_revision(&self.user_uuid, conn).await; @@ -1015,5 +1078,6 @@ mod tests { assert!(UserOrgType::Owner > UserOrgType::Admin); assert!(UserOrgType::Admin > UserOrgType::Manager); assert!(UserOrgType::Manager > UserOrgType::User); + assert!(UserOrgType::Manager == UserOrgType::from_str("4").unwrap()); } } diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 3fc3e70ed2..16bc1ee410 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -42,12 +42,6 @@ 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; @@ -58,10 +52,34 @@ app-organization-plans > form > bit-section:nth-child(2) { @extend %vw-hide; } +/* Hide Collection Management Form */ +app-org-account form.ng-untouched:nth-child(6) { + display:none !important +} + /* Hide Device Verification form at the Two Step Login screen */ app-security > app-two-factor-setup > form { @extend %vw-hide; } + +/* Hide unsupported Custom Role options */ +bit-dialog div.tw-ml-4:has(bit-form-control input), +bit-dialog div.tw-col-span-4:has(input[formcontrolname*="access"], input[formcontrolname*="manage"]) { + @extend %vw-hide; +} + +/* Change collapsed menu icon to Vaultwarden */ +bit-nav-logo bit-nav-item a:before { + content: ""; + background-image: url("../images/icon-white.svg"); + background-repeat: no-repeat; + background-position: center center; + height: 32px; + display: block; +} +bit-nav-logo bit-nav-item .bwi-shield { + @extend %vw-hide; +} /**** END Static Vaultwarden Changes ****/ /**** START Dynamic Vaultwarden Changes ****/ {{#if signup_disabled}} @@ -73,14 +91,14 @@ app-frontend-layout > app-login > form > div > div > div > p { /* 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) { +app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(1) { @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) { +app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(4) { @extend %vw-hide; } {{/unless}} From ec2ed57c04ac13f9430b9763b64d14622b0f91e0 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 7 Dec 2024 11:41:29 +0100 Subject: [PATCH 2/7] Fix hide passkey login Signed-off-by: BlackDex --- src/static/templates/scss/vaultwarden.scss.hbs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 16bc1ee410..fc15130a1a 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -68,6 +68,11 @@ bit-dialog div.tw-col-span-4:has(input[formcontrolname*="access"], input[formcon @extend %vw-hide; } +/* Hide Log in with passkey */ +app-login div.tw-flex:nth-child(4) { + @extend %vw-hide; +} + /* Change collapsed menu icon to Vaultwarden */ bit-nav-logo bit-nav-item a:before { content: ""; From 0e6f3a491b4c85b690196dcaefc2dd31fcb8441c Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 7 Dec 2024 11:52:14 +0100 Subject: [PATCH 3/7] Fix hide create account Signed-off-by: BlackDex --- src/static/templates/scss/vaultwarden.scss.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index fc15130a1a..944c0c93ce 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -89,7 +89,7 @@ bit-nav-logo bit-nav-item .bwi-shield { /**** 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 { +app-login form p { @extend %vw-hide; } {{/if}} From 4e59ba23251c411a04abee2c42de196f22c235d4 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Thu, 12 Dec 2024 00:04:16 +0100 Subject: [PATCH 4/7] Small changes for v2024.12.0 Signed-off-by: BlackDex --- src/api/core/organizations.rs | 17 +++++++++++++++-- src/static/templates/scss/vaultwarden.scss.hbs | 7 ++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 2551315981..b1a4dd033e 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -78,6 +78,7 @@ pub fn routes() -> Vec { restore_organization_user, bulk_restore_organization_user, get_groups, + get_groups_details, post_groups, get_group, put_group, @@ -99,6 +100,7 @@ pub fn routes() -> Vec { get_org_export, api_key, rotate_api_key, + get_billing_metadata, ] } @@ -1369,7 +1371,7 @@ async fn edit_user( ) -> EmptyResult { let mut data: EditUserData = data.into_inner(); - // HACK: We need the raw user-type be be sure custom role is selected to determine the access_all permission + // HACK: We need the raw user-type be be sure custom role is selected to determine the access_all permission // The from_str() will convert the custom role type into a manager role type let raw_type = &data.r#type.into_string(); // UserOrgType::from_str will convert custom (4) to manager (3) @@ -1981,6 +1983,12 @@ fn get_plans_tax_rates(_headers: Headers) -> Json { Json(_empty_data_json()) } +#[get("/organizations/<_org_id>/billing/metadata")] +fn get_billing_metadata(_org_id: &str, _headers: Headers) -> Json { + // Prevent a 404 error, which also causes Javascript errors. + Json(_empty_data_json()) +} + fn _empty_data_json() -> Value { json!({ "object": "list", @@ -2379,6 +2387,11 @@ async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbCon }))) } +#[get("/organizations//groups/details", rank = 1)] +async fn get_groups_details(org_id: &str, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { + get_groups(org_id, headers, conn).await +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct GroupRequest { @@ -2628,7 +2641,7 @@ async fn bulk_delete_groups( Ok(()) } -#[get("/organizations//groups/")] +#[get("/organizations//groups/", rank = 2)] async fn get_group(org_id: &str, group_id: &str, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 944c0c93ce..0f707b3ac0 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -54,7 +54,12 @@ app-organization-plans > form > bit-section:nth-child(2) { /* Hide Collection Management Form */ app-org-account form.ng-untouched:nth-child(6) { - display:none !important + @extend %vw-hide; +} + +/* Hide 'Member Access' Report Card from Org Reports */ +app-org-reports-home > app-report-list > div.tw-inline-grid > div:nth-child(6) { + @extend %vw-hide; } /* Hide Device Verification form at the Two Step Login screen */ From 84f01d2c12149ce2ada703bb9db9bea0eea25d27 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Tue, 17 Dec 2024 19:32:49 +0100 Subject: [PATCH 5/7] Fix hide create account link Signed-off-by: BlackDex --- src/static/templates/scss/vaultwarden.scss.hbs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs index 0f707b3ac0..42c4d8dce7 100644 --- a/src/static/templates/scss/vaultwarden.scss.hbs +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -94,34 +94,35 @@ bit-nav-logo bit-nav-item .bwi-shield { /**** START Dynamic Vaultwarden Changes ****/ {{#if signup_disabled}} /* Hide the register link on the login screen */ -app-login form p { +app-login form div + div + div + div + hr, +app-login form div + div + div + div + hr + p { @extend %vw-hide; } {{/if}} -/* Hide `Email` 2FA if mail is not enabled */ {{#unless mail_enabled}} +/* Hide `Email` 2FA if mail is not enabled */ app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(1) { @extend %vw-hide; } {{/unless}} -/* Hide `YubiKey OTP security key` 2FA if it is not enabled */ {{#unless yubico_enabled}} +/* Hide `YubiKey OTP security key` 2FA if it is not enabled */ app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(4) { @extend %vw-hide; } {{/unless}} -/* Hide Emergency Access if not allowed */ {{#unless emergency_access_allowed}} +/* Hide Emergency Access if not allowed */ bit-nav-item[route="settings/emergency-access"] { @extend %vw-hide; } {{/unless}} -/* Hide Sends if not allowed */ {{#unless sends_allowed}} +/* Hide Sends if not allowed */ bit-nav-item[route="sends"] { @extend %vw-hide; } From 296e45bc6ce24edb888bd0bfaa92eefc6c6730bd Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 28 Dec 2024 17:26:58 +0100 Subject: [PATCH 6/7] Add pre-release web-vault Signed-off-by: BlackDex --- docker/DockerSettings.yaml | 8 ++++---- docker/Dockerfile.alpine | 12 ++++++------ docker/Dockerfile.debian | 14 +++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docker/DockerSettings.yaml b/docker/DockerSettings.yaml index 6896c3dd47..a756972bc4 100644 --- a/docker/DockerSettings.yaml +++ b/docker/DockerSettings.yaml @@ -1,10 +1,10 @@ --- -vault_version: "v2024.6.2c" -vault_image_digest: "sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b" -# Cross Compile Docker Helper Scripts v1.5.0 +vault_version: "v2024.12.0" +vault_image_digest: "sha256:75a537ea5a4077bf5042b40094b7aa12cf53fecbb5483a1547b544dd6397c5fb" +# Cross Compile Docker Helper Scripts v1.6.1 # We use the linux/amd64 platform shell scripts since there is no difference between the different platform scripts # https://github.com/tonistiigi/xx | https://hub.docker.com/r/tonistiigi/xx/tags -xx_image_digest: "sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa" +xx_image_digest: "sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894" rust_version: 1.83.0 # Rust version to be used debian_version: bookworm # Debian release name to be used alpine_version: "3.21" # Alpine version to be used diff --git a/docker/Dockerfile.alpine b/docker/Dockerfile.alpine index 77915fd8ca..88747d9795 100644 --- a/docker/Dockerfile.alpine +++ b/docker/Dockerfile.alpine @@ -19,15 +19,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2c -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c -# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b] +# $ docker pull docker.io/vaultwarden/web-vault:v2024.12.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.12.0 +# [docker.io/vaultwarden/web-vault@sha256:75a537ea5a4077bf5042b40094b7aa12cf53fecbb5483a1547b544dd6397c5fb] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b -# [docker.io/vaultwarden/web-vault:v2024.6.2c] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:75a537ea5a4077bf5042b40094b7aa12cf53fecbb5483a1547b544dd6397c5fb +# [docker.io/vaultwarden/web-vault:v2024.12.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:75a537ea5a4077bf5042b40094b7aa12cf53fecbb5483a1547b544dd6397c5fb AS vault ########################## ALPINE BUILD IMAGES ########################## ## NOTE: The Alpine Base Images do not support other platforms then linux/amd64 diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 69404a2ec0..788156171b 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -19,20 +19,20 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull docker.io/vaultwarden/web-vault:v2024.6.2c -# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.6.2c -# [docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b] +# $ docker pull docker.io/vaultwarden/web-vault:v2024.12.0 +# $ docker image inspect --format "{{.RepoDigests}}" docker.io/vaultwarden/web-vault:v2024.12.0 +# [docker.io/vaultwarden/web-vault@sha256:75a537ea5a4077bf5042b40094b7aa12cf53fecbb5483a1547b544dd6397c5fb] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b -# [docker.io/vaultwarden/web-vault:v2024.6.2c] +# $ docker image inspect --format "{{.RepoTags}}" docker.io/vaultwarden/web-vault@sha256:75a537ea5a4077bf5042b40094b7aa12cf53fecbb5483a1547b544dd6397c5fb +# [docker.io/vaultwarden/web-vault:v2024.12.0] # -FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:409ab328ca931439cb916b388a4bb784bd44220717aaf74cf71620c23e34fc2b AS vault +FROM --platform=linux/amd64 docker.io/vaultwarden/web-vault@sha256:75a537ea5a4077bf5042b40094b7aa12cf53fecbb5483a1547b544dd6397c5fb AS vault ########################## Cross Compile Docker Helper Scripts ########################## ## We use the linux/amd64 no matter which Build Platform, since these are all bash scripts ## And these bash scripts do not have any significant difference if at all -FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:1978e7a58a1777cb0ef0dde76bad60b7914b21da57cfa88047875e4f364297aa AS xx +FROM --platform=linux/amd64 docker.io/tonistiigi/xx@sha256:9c207bead753dda9430bdd15425c6518fc7a03d866103c516a2c6889188f5894 AS xx ########################## BUILD IMAGE ########################## # hadolint ignore=DL3006 From 48116b0d1777b8fcc99699ee63f085bdd3a78b34 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Mon, 30 Dec 2024 17:15:53 +0100 Subject: [PATCH 7/7] Rename function to mention swapping uuid's Signed-off-by: BlackDex --- src/api/core/organizations.rs | 12 ++++++------ src/db/models/collection.rs | 5 ++++- src/db/models/organization.rs | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index b1a4dd033e..902ab25a12 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -325,7 +325,7 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose, }; // get all collection memberships for the current organization - let coll_users = CollectionUser::find_by_organization(org_id, &mut conn).await; + let coll_users = CollectionUser::find_by_organization_swap_user_uuid_with_org_user_uuid(org_id, &mut conn).await; // Generate a HashMap to get the correct UserOrgType per user to determine the manage permission // We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser let users_org_type: HashMap = UserOrganization::find_confirmed_by_org(org_id, &mut conn) @@ -871,7 +871,7 @@ struct InviteData { async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { let mut data: InviteData = data.into_inner(); - // HACK: We need the raw user-type be be sure custom role is selected to determine the access_all permission + // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission // The from_str() will convert the custom role type into a manager role type let raw_type = &data.r#type.into_string(); // UserOrgType::from_str will convert custom (4) to manager (3) @@ -885,7 +885,7 @@ async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders } // HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag - // Since the parent checkbox is not send to the server we need to check and verify the child checkboxes + // Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes // If the box is not checked, the user will still be a manager, but not with the access_all permission if raw_type.eq("4") && data.permissions.get("editAnyCollection") == Some(&json!(true)) @@ -1371,7 +1371,7 @@ async fn edit_user( ) -> EmptyResult { let mut data: EditUserData = data.into_inner(); - // HACK: We need the raw user-type be be sure custom role is selected to determine the access_all permission + // HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission // The from_str() will convert the custom role type into a manager role type let raw_type = &data.r#type.into_string(); // UserOrgType::from_str will convert custom (4) to manager (3) @@ -1380,7 +1380,7 @@ async fn edit_user( }; // HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag - // Since the parent checkbox is not send to the server we need to check and verify the child checkboxes + // Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes // If the box is not checked, the user will still be a manager, but not with the access_all permission if raw_type.eq("4") && data.permissions.get("editAnyCollection") == Some(&json!(true)) @@ -3011,7 +3011,7 @@ async fn put_reset_password_enrollment( if reset_request.reset_password_key.is_none() && OrgPolicy::org_is_reset_password_auto_enroll(org_id, &mut conn).await { - err!("Reset password can't be withdrawed due to an enterprise policy"); + err!("Reset password can't be withdrawn due to an enterprise policy"); } if reset_request.reset_password_key.is_some() { diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index a26f22c72e..907aebf7a8 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -511,7 +511,10 @@ impl CollectionUser { }} } - pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_organization_swap_user_uuid_with_org_user_uuid( + org_uuid: &str, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_collections::table .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 5f8434dea9..c8c1d3a459 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -652,7 +652,7 @@ impl UserOrganization { pub async fn to_json_mini_details(&self, conn: &mut DbConn) -> Value { let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap(); - // Because BitWarden want the status to be -1 for revoked users we need to catch that here. + // Because Bitwarden wants the status to be -1 for revoked users we need to catch that here. // We subtract/add a number so we can restore/activate the user to it's previous state again. let status = if self.status < UserOrgStatus::Revoked as i32 { UserOrgStatus::Revoked as i32