From adacddafd2e6b56ff809df77e2fccbb5faa8d4f4 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Fri, 6 Dec 2024 17:09:26 +0100 Subject: [PATCH 1/5] 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 f315853699..0f91e1c2ae 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 8daf35f43a..b11086280b 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(|| { @@ -1326,6 +1326,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) => {{ @@ -1429,3 +1431,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 b60a8ec3d5a3019ed1a9b9dea5579da24839d82f Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 7 Dec 2024 11:41:29 +0100 Subject: [PATCH 2/5] 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 f958731865f2576fac4e7ee7b1d8a28ee2f74b35 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Sat, 7 Dec 2024 11:52:14 +0100 Subject: [PATCH 3/5] 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 b0920a621c01f255753f3a878c0851d2addc861e Mon Sep 17 00:00:00 2001 From: BlackDex Date: Thu, 12 Dec 2024 00:04:16 +0100 Subject: [PATCH 4/5] 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 0f91e1c2ae..b31efb6344 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 a85c9e06456a5d4c64ccd29e56f6781313855cf5 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Tue, 17 Dec 2024 19:32:49 +0100 Subject: [PATCH 5/5] 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; }