diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index f315853699..b31efb6344 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, @@ -77,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, @@ -98,6 +100,7 @@ pub fn routes() -> Vec { get_org_export, api_key, rotate_api_key, + get_billing_metadata, ] } @@ -323,6 +326,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 +346,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 +666,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 +863,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 +884,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 +1304,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 +1346,8 @@ struct EditUserData { groups: Option>, #[serde(default)] access_all: bool, + #[serde(default)] + permissions: HashMap, } #[put("/organizations//users/", data = "", rank = 1)] @@ -1303,14 +1369,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 @@ -1901,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", @@ -2299,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 { @@ -2331,6 +2424,7 @@ struct SelectionReadOnly { id: String, read_only: bool, hide_passwords: bool, + manage: bool, } impl SelectionReadOnly { @@ -2339,18 +2433,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), } } @@ -2534,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/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..42c4d8dce7 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,42 +52,77 @@ 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) { + @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 */ 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; +} + +/* 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: ""; + 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}} /* Hide the register link on the login screen */ -app-frontend-layout > app-login > form > div > div > div > 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}} -app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(5) { +/* 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}} -app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(2) { +/* 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; }