1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 01:33:33 +00:00

Merge branch 'main' into auth/pm-14369/hide-account-switcher-if-on-login-page-and-not-logged-in

This commit is contained in:
Alec Rippberger
2024-11-04 11:35:47 -06:00
committed by GitHub
39 changed files with 4615 additions and 7368 deletions

View File

@@ -73,7 +73,7 @@
"reviewers": ["team:team-admin-console-dev"] "reviewers": ["team:team-admin-console-dev"]
}, },
{ {
"matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious", "regedit"], "matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious"],
"description": "Auth owned dependencies", "description": "Auth owned dependencies",
"commitMessagePrefix": "[deps] Auth:", "commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"] "reviewers": ["team:team-auth-dev"]
@@ -258,5 +258,5 @@
"reviewers": ["team:team-vault-dev"] "reviewers": ["team:team-vault-dev"]
} }
], ],
"ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm", "regedit"] "ignoreDeps": ["@types/koa-bodyparser", "bootstrap", "node-ipc", "node", "npm"]
} }

View File

@@ -128,10 +128,10 @@
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "test-storybook:build:production" "buildTarget": "test-storybook:build:production"
}, },
"development": { "development": {
"browserTarget": "test-storybook:build:development" "buildTarget": "test-storybook:build:development"
} }
}, },
"defaultConfiguration": "development" "defaultConfiguration": "development"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/browser", "name": "@bitwarden/browser",
"version": "2024.10.1", "version": "2024.11.0",
"scripts": { "scripts": {
"build": "cross-env MANIFEST_VERSION=3 webpack", "build": "cross-env MANIFEST_VERSION=3 webpack",
"build:mv2": "webpack", "build:mv2": "webpack",

View File

@@ -32,6 +32,7 @@ import {
} from "@bitwarden/common/spec"; } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -106,6 +107,7 @@ describe("OverlayBackground", () => {
let selectedThemeMock$: BehaviorSubject<ThemeType>; let selectedThemeMock$: BehaviorSubject<ThemeType>;
let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
let themeStateService: MockProxy<ThemeStateService>; let themeStateService: MockProxy<ThemeStateService>;
let totpService: MockProxy<TotpService>;
let overlayBackground: OverlayBackground; let overlayBackground: OverlayBackground;
let portKeyForTabSpy: Record<number, string>; let portKeyForTabSpy: Record<number, string>;
let pageDetailsForTabSpy: PageDetailsForTab; let pageDetailsForTabSpy: PageDetailsForTab;
@@ -184,6 +186,7 @@ describe("OverlayBackground", () => {
inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
themeStateService = mock<ThemeStateService>(); themeStateService = mock<ThemeStateService>();
themeStateService.selectedTheme$ = selectedThemeMock$; themeStateService.selectedTheme$ = selectedThemeMock$;
totpService = mock<TotpService>();
overlayBackground = new OverlayBackground( overlayBackground = new OverlayBackground(
logService, logService,
cipherService, cipherService,
@@ -198,6 +201,7 @@ describe("OverlayBackground", () => {
fido2ActiveRequestManager, fido2ActiveRequestManager,
inlineMenuFieldQualificationService, inlineMenuFieldQualificationService,
themeStateService, themeStateService,
totpService,
generatedPasswordCallbackMock, generatedPasswordCallbackMock,
addPasswordCallbackMock, addPasswordCallbackMock,
); );

View File

@@ -33,6 +33,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
@@ -217,6 +218,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private fido2ActiveRequestManager: Fido2ActiveRequestManager, private fido2ActiveRequestManager: Fido2ActiveRequestManager,
private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService, private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService,
private themeStateService: ThemeStateService, private themeStateService: ThemeStateService,
private totpService: TotpService,
private generatePasswordCallback: () => Promise<string>, private generatePasswordCallback: () => Promise<string>,
private addPasswordCallback: (password: string) => Promise<void>, private addPasswordCallback: (password: string) => Promise<void>,
) { ) {
@@ -1058,7 +1060,6 @@ export class OverlayBackground implements OverlayBackgroundInterface {
} }
const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId);
if (usePasskey && cipher.login?.hasFido2Credentials) { if (usePasskey && cipher.login?.hasFido2Credentials) {
await this.authenticatePasskeyCredential( await this.authenticatePasskeyCredential(
sender, sender,
@@ -1066,6 +1067,11 @@ export class OverlayBackground implements OverlayBackgroundInterface {
); );
this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher);
if (cipher.login?.totp) {
this.platformUtilsService.copyToClipboard(
await this.totpService.getCode(cipher.login.totp),
);
}
return; return;
} }

View File

@@ -1678,6 +1678,7 @@ export default class MainBackground {
this.fido2ActiveRequestManager, this.fido2ActiveRequestManager,
inlineMenuFieldQualificationService, inlineMenuFieldQualificationService,
this.themeStateService, this.themeStateService,
this.totpService,
() => this.generatePassword(), () => this.generatePassword(),
(password) => this.addPasswordToHistory(password), (password) => this.addPasswordToHistory(password),
); );

View File

@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.10.1", "version": "2024.11.0",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0", "minimum_chrome_version": "102.0",
"name": "__MSG_extName__", "name": "__MSG_extName__",
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__",
"version": "2024.10.1", "version": "2024.11.0",
"description": "__MSG_extDesc__", "description": "__MSG_extDesc__",
"default_locale": "en", "default_locale": "en",
"author": "Bitwarden Inc.", "author": "Bitwarden Inc.",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@bitwarden/cli", "name": "@bitwarden/cli",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.10.0", "version": "2024.11.0",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",
@@ -80,7 +80,7 @@
"papaparse": "5.4.1", "papaparse": "5.4.1",
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tldts": "6.1.56", "tldts": "6.1.58",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"
} }
} }

View File

@@ -546,6 +546,7 @@ dependencies = [
"napi-derive", "napi-derive",
"tokio", "tokio",
"tokio-util", "tokio-util",
"windows-registry",
] ]
[[package]] [[package]]
@@ -2226,7 +2227,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [ dependencies = [
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
"windows-result", "windows-result 0.1.2",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@@ -2252,6 +2253,17 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "windows-registry"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a"
dependencies = [
"windows-result 0.2.0",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.1.2" version = "0.1.2"
@@ -2261,6 +2273,24 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View File

@@ -21,5 +21,8 @@ napi-derive = "=2.16.12"
tokio = { version = "1.38.0" } tokio = { version = "1.38.0" }
tokio-util = "0.7.11" tokio-util = "0.7.11"
[target.'cfg(windows)'.dependencies]
windows-registry = "=0.3.0"
[build-dependencies] [build-dependencies]
napi-build = "=2.1.3" napi-build = "=2.1.3"

View File

@@ -51,6 +51,10 @@ export declare namespace powermonitors {
export function onLock(callback: (err: Error | null, ) => any): Promise<void> export function onLock(callback: (err: Error | null, ) => any): Promise<void>
export function isLockMonitorAvailable(): Promise<boolean> export function isLockMonitorAvailable(): Promise<boolean>
} }
export declare namespace windows_registry {
export function createKey(key: string, subkey: string, value: string): Promise<void>
export function deleteKey(key: string, subkey: string): Promise<void>
}
export declare namespace ipc { export declare namespace ipc {
export interface IpcMessage { export interface IpcMessage {
clientId: number clientId: number

View File

@@ -1,5 +1,8 @@
#[macro_use] #[macro_use]
extern crate napi_derive; extern crate napi_derive;
mod registry;
#[napi] #[napi]
pub mod passwords { pub mod passwords {
/// Fetch the stored password from the keychain. /// Fetch the stored password from the keychain.
@@ -190,6 +193,21 @@ pub mod powermonitors {
} }
#[napi]
pub mod windows_registry {
#[napi]
pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> {
crate::registry::create_key(&key, &subkey, &value)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> {
crate::registry::delete_key(&key, &subkey)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi] #[napi]
pub mod ipc { pub mod ipc {
use desktop_core::ipc::server::{Message, MessageType}; use desktop_core::ipc::server::{Message, MessageType};

View File

@@ -0,0 +1,9 @@
use anyhow::{bail, Result};
pub fn create_key(_key: &str, _subkey: &str, _value: &str) -> Result<()> {
bail!("Not implemented")
}
pub fn delete_key(_key: &str, _subkey: &str) -> Result<()> {
bail!("Not implemented")
}

View File

@@ -0,0 +1,4 @@
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(not(target_os = "windows"), path = "dummy.rs")]
mod internal;
pub use internal::*;

View File

@@ -0,0 +1,29 @@
use anyhow::{bail, Result};
fn convert_key(key: &str) -> Result<&'static windows_registry::Key> {
Ok(match key.to_uppercase().as_str() {
"HKEY_CURRENT_USER" | "HKCU" => windows_registry::CURRENT_USER,
"HKEY_LOCAL_MACHINE" | "HKLM" => windows_registry::LOCAL_MACHINE,
"HKEY_CLASSES_ROOT" | "HKCR" => windows_registry::CLASSES_ROOT,
_ => bail!("Invalid key"),
})
}
pub fn create_key(key: &str, subkey: &str, value: &str) -> Result<()> {
let key = convert_key(key)?;
let subkey = key.create(subkey)?;
const DEFAULT: &str = "";
subkey.set_string(DEFAULT, value)?;
Ok(())
}
pub fn delete_key(key: &str, subkey: &str) -> Result<()> {
let key = convert_key(key)?;
key.remove_tree(subkey)?;
Ok(())
}

View File

@@ -90,13 +90,6 @@
"electronUpdaterCompatibility": ">=0.0.1", "electronUpdaterCompatibility": ">=0.0.1",
"target": ["portable", "nsis-web", "appx"], "target": ["portable", "nsis-web", "appx"],
"sign": "./sign.js", "sign": "./sign.js",
"extraResources": [
{
"from": "../../node_modules/regedit/vbs",
"to": "regedit/vbs",
"filter": ["**/*"]
}
],
"extraFiles": [ "extraFiles": [
{ {
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.10.3", "version": "2024.11.0",
"keywords": [ "keywords": [
"bitwarden", "bitwarden",
"password", "password",

View File

@@ -1,12 +1,11 @@
import { existsSync, promises as fs } from "fs"; import { existsSync, promises as fs } from "fs";
import { homedir, userInfo } from "os"; import { homedir, userInfo } from "os";
import * as path from "path"; import * as path from "path";
import * as util from "util";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ipc } from "@bitwarden/desktop-napi"; import { ipc, windows_registry } from "@bitwarden/desktop-napi";
import { isDev } from "../utils"; import { isDev } from "../utils";
@@ -142,12 +141,12 @@ export class NativeMessagingMain {
await this.writeManifest(path.join(destination, "chrome.json"), chromeJson); await this.writeManifest(path.join(destination, "chrome.json"), chromeJson);
const nmhs = this.getWindowsNMHS(); const nmhs = this.getWindowsNMHS();
for (const [key, value] of Object.entries(nmhs)) { for (const [name, [key, subkey]] of Object.entries(nmhs)) {
let manifestPath = path.join(destination, "chrome.json"); let manifestPath = path.join(destination, "chrome.json");
if (key === "Firefox") { if (name === "Firefox") {
manifestPath = path.join(destination, "firefox.json"); manifestPath = path.join(destination, "firefox.json");
} }
await this.createWindowsRegistry(value, manifestPath); await windows_registry.createKey(key, subkey, manifestPath);
} }
break; break;
} }
@@ -225,8 +224,8 @@ export class NativeMessagingMain {
await this.removeIfExists(path.join(this.userPath, "browsers", "chrome.json")); await this.removeIfExists(path.join(this.userPath, "browsers", "chrome.json"));
const nmhs = this.getWindowsNMHS(); const nmhs = this.getWindowsNMHS();
for (const [, value] of Object.entries(nmhs)) { for (const [, [key, subkey]] of Object.entries(nmhs)) {
await this.deleteWindowsRegistry(value); await windows_registry.deleteKey(key, subkey);
} }
break; break;
} }
@@ -274,11 +273,14 @@ export class NativeMessagingMain {
private getWindowsNMHS() { private getWindowsNMHS() {
return { return {
Firefox: "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", Firefox: ["HKCU", "SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden"],
Chrome: "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", Chrome: ["HKCU", "SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden"],
Chromium: "HKCU\\SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden", Chromium: ["HKCU", "SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden"],
// Edge uses the same registry key as Chrome as a fallback, but it's has its own separate key as well. // Edge uses the same registry key as Chrome as a fallback, but it's has its own separate key as well.
"Microsoft Edge": "HKCU\\SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden", "Microsoft Edge": [
"HKCU",
"SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden",
],
}; };
} }
@@ -419,52 +421,6 @@ export class NativeMessagingMain {
return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`); return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`);
} }
private getRegeditInstance() {
// eslint-disable-next-line
const regedit = require("regedit");
regedit.setExternalVBSLocation(path.join(path.dirname(this.exePath), "resources/regedit/vbs"));
return regedit;
}
private async createWindowsRegistry(location: string, jsonFile: string) {
const regedit = this.getRegeditInstance();
const createKey = util.promisify(regedit.createKey);
const putValue = util.promisify(regedit.putValue);
this.logService.debug(`Adding registry: ${location}`);
await createKey(location);
// Insert path to manifest
const obj: any = {};
obj[location] = {
default: {
value: jsonFile,
type: "REG_DEFAULT",
},
};
return putValue(obj);
}
private async deleteWindowsRegistry(key: string) {
const regedit = this.getRegeditInstance();
const list = util.promisify(regedit.list);
const deleteKey = util.promisify(regedit.deleteKey);
this.logService.debug(`Removing registry: ${key}`);
try {
await list(key);
await deleteKey(key);
} catch {
this.logService.error(`Unable to delete registry key: ${key}`);
}
}
private homedir() { private homedir() {
if (process.platform === "darwin") { if (process.platform === "darwin") {
return userInfo().homedir; return userInfo().homedir;

View File

@@ -1,12 +1,12 @@
{ {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.10.3", "version": "2024.11.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"version": "2024.10.3", "version": "2024.11.0",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi", "@bitwarden/desktop-napi": "file:../desktop_native/napi",

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop", "name": "@bitwarden/desktop",
"productName": "Bitwarden", "productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.", "description": "A secure and free password manager for all of your devices.",
"version": "2024.10.3", "version": "2024.11.0",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)", "author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com", "homepage": "https://bitwarden.com",
"license": "GPL-3.0", "license": "GPL-3.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/web-vault", "name": "@bitwarden/web-vault",
"version": "2024.10.5", "version": "2024.11.0",
"scripts": { "scripts": {
"build:oss": "webpack", "build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@@ -23,15 +23,14 @@
<bit-tab [label]="'role' | i18n"> <bit-tab [label]="'role' | i18n">
<ng-container *ngIf="!editMode"> <ng-container *ngIf="!editMode">
<p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p> <p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p>
<bit-form-field *ngIf="remainingSeats$ | async as remainingSeats"> <bit-form-field>
<bit-label>{{ "email" | i18n }}</bit-label> <bit-label>{{ "email" | i18n }}</bit-label>
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" /> <input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
<bit-hint *ngIf="remainingSeats > 1; else singleSeat">{{ <bit-hint>{{
"inviteMultipleEmailDesc" | i18n: remainingSeats "inviteMultipleEmailDesc"
| i18n
: (organization.productTierType === ProductTierType.TeamsStarter ? "10" : "20")
}}</bit-hint> }}</bit-hint>
<ng-template #singleSeat>
<bit-hint>{{ "inviteSingleEmailDesc" | i18n: remainingSeats }}</bit-hint>
</ng-template>
</bit-form-field> </bit-form-field>
</ng-container> </ng-container>
<bit-radio-group formControlName="type"> <bit-radio-group formControlName="type">
@@ -265,6 +264,16 @@
<button <button
*ngIf="editMode" *ngIf="editMode"
type="button" type="button"
bitIconButton="bwi-close"
buttonType="danger"
bitFormButton
[appA11yTitle]="'remove' | i18n"
[bitAction]="remove"
[disabled]="loading"
></button>
<button
*ngIf="editMode && params.managedByOrganization === true"
type="button"
bitIconButton="bwi-trash" bitIconButton="bwi-trash"
buttonType="danger" buttonType="danger"
bitFormButton bitFormButton

View File

@@ -65,6 +65,7 @@ export interface MemberDialogParams {
isOnSecretsManagerStandalone: boolean; isOnSecretsManagerStandalone: boolean;
initialTab?: MemberDialogTab; initialTab?: MemberDialogTab;
numConfirmedMembers: number; numConfirmedMembers: number;
managedByOrganization?: boolean;
} }
export enum MemberDialogResult { export enum MemberDialogResult {
@@ -89,7 +90,6 @@ export class MemberDialogComponent implements OnDestroy {
PermissionMode = PermissionMode; PermissionMode = PermissionMode;
showNoMasterPasswordWarning = false; showNoMasterPasswordWarning = false;
isOnSecretsManagerStandalone: boolean; isOnSecretsManagerStandalone: boolean;
remainingSeats$: Observable<number>;
protected organization$: Observable<Organization>; protected organization$: Observable<Organization>;
protected collectionAccessItems: AccessItemView[] = []; protected collectionAccessItems: AccessItemView[] = [];
@@ -251,10 +251,6 @@ export class MemberDialogComponent implements OnDestroy {
this.loading = false; this.loading = false;
}); });
this.remainingSeats$ = this.organization$.pipe(
map((organization) => organization.seats - this.params.numConfirmedMembers),
);
} }
private setFormValidators(organization: Organization) { private setFormValidators(organization: Organization) {
@@ -469,7 +465,7 @@ export class MemberDialogComponent implements OnDestroy {
this.close(MemberDialogResult.Saved); this.close(MemberDialogResult.Saved);
}; };
delete = async () => { remove = async () => {
if (!this.editMode) { if (!this.editMode) {
return; return;
} }
@@ -566,6 +562,39 @@ export class MemberDialogComponent implements OnDestroy {
this.close(MemberDialogResult.Restored); this.close(MemberDialogResult.Restored);
}; };
delete = async () => {
if (!this.editMode) {
return;
}
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
placeholders: [this.params.name],
},
content: { key: "deleteOrganizationUserWarning" },
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
await this.organizationUserApiService.deleteOrganizationUser(
this.params.organizationId,
this.params.organizationUserId,
);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("organizationUserDeleted", this.params.name),
});
this.close(MemberDialogResult.Deleted);
};
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();

View File

@@ -320,6 +320,17 @@
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }} <i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span> </span>
</button> </button>
<button
*ngIf="u.managedByOrganization === true"
type="button"
bitMenuItem
(click)="deleteUser(u)"
>
<span class="tw-text-danger">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
</span>
</button>
</bit-menu> </bit-menu>
</td> </td>
</tr> </tr>

View File

@@ -518,6 +518,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
initialTab: initialTab, initialTab: initialTab,
numConfirmedMembers: this.dataSource.confirmedUserCount, numConfirmedMembers: this.dataSource.confirmedUserCount,
managedByOrganization: user?.managedByOrganization,
}, },
}); });
@@ -725,6 +726,40 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return true; return true;
} }
async deleteUser(user: OrganizationUserView) {
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "deleteOrganizationUser",
placeholders: [this.userNamePipe.transform(user)],
},
content: { key: "deleteOrganizationUserWarning" },
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
});
if (!confirmed) {
return false;
}
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
this.organization.id,
user.id,
);
try {
await this.actionPromise;
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("organizationUserDeleted", this.userNamePipe.transform(user)),
});
this.dataSource.removeUser(user);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) { private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
return this.dialogService.openSimpleDialog({ return this.dialogService.openSimpleDialog({
title: { title: {

View File

@@ -21,7 +21,13 @@
> >
{{ "purgeVault" | i18n }} {{ "purgeVault" | i18n }}
</button> </button>
<button type="button" bitButton buttonType="danger" [bitAction]="deleteAccount"> <button
*ngIf="showDeleteAccount$ | async"
type="button"
bitButton
buttonType="danger"
[bitAction]="deleteAccount"
>
{{ "deleteAccount" | i18n }} {{ "deleteAccount" | i18n }}
</button> </button>
</app-danger-zone> </app-danger-zone>

View File

@@ -23,6 +23,7 @@ export class AccountComponent implements OnInit {
showChangeEmail$: Observable<boolean>; showChangeEmail$: Observable<boolean>;
showPurgeVault$: Observable<boolean>; showPurgeVault$: Observable<boolean>;
showDeleteAccount$: Observable<boolean>;
constructor( constructor(
private modalService: ModalService, private modalService: ModalService,
@@ -63,6 +64,16 @@ export class AccountComponent implements OnInit {
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization, !isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
), ),
); );
this.showDeleteAccount$ = combineLatest([
isAccountDeprovisioningEnabled$,
userIsManagedByOrganization$,
]).pipe(
map(
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
);
} }
async deauthorizeSessions() { async deauthorizeSessions() {

View File

@@ -3218,9 +3218,6 @@
} }
} }
}, },
"inviteSingleEmailDesc": {
"message": "You have 1 invite remaining."
},
"userUsingTwoStep": { "userUsingTwoStep": {
"message": "This user is using two-step login to protect their account." "message": "This user is using two-step login to protect their account."
}, },
@@ -9557,5 +9554,31 @@
}, },
"single-org-revoked-user-warning": { "single-org-revoked-user-warning": {
"message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations." "message": "Non-compliant members will be revoked. Administrators can restore members once they leave all other organizations."
},
"deleteOrganizationUser": {
"message": "Delete $NAME$",
"placeholders": {
"name": {
"content": "$1",
"example": "John Doe"
},
"description": "Title for the delete organization user dialog"
}
},
"deleteOrganizationUserWarning": {
"message": "When a member is deleted, their Bitwarden account and individual vault data will be permanently deleted. Collection data will remain in the organization. To reinstate them they must create an account and be onboarded again.",
"description": "Warning for the delete organization user dialog"
},
"organizationUserDeleted": {
"message": "Deleted $NAME$",
"placeholders": {
"name": {
"content": "$1",
"example": "John Doe"
}
}
},
"organizationUserDeletedDesc": {
"message": "The user was removed from the organization and all associated user data has been deleted."
} }
} }

View File

@@ -1,10 +1,9 @@
import "core-js/stable"; import "core-js/stable";
require("zone.js/dist/zone"); import "zone.js";
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {
// Production // Production
} else { } else {
// Development and test // Development and test
Error["stackTraceLimit"] = Infinity; Error["stackTraceLimit"] = Infinity;
require("zone.js/dist/long-stack-trace-zone");
} }

View File

@@ -275,4 +275,11 @@ export abstract class OrganizationUserApiService {
organizationId: string, organizationId: string,
ids: string[], ids: string[],
): Promise<ListResponse<OrganizationUserBulkResponse>>; ): Promise<ListResponse<OrganizationUserBulkResponse>>;
/**
* Remove an organization user's access to the organization and delete their account data
* @param organizationId - Identifier for the organization the user belongs to
* @param id - Organization user identifier
*/
abstract deleteOrganizationUser(organizationId: string, id: string): Promise<void>;
} }

View File

@@ -359,4 +359,14 @@ export class DefaultOrganizationUserApiService implements OrganizationUserApiSer
); );
return new ListResponse(r, OrganizationUserBulkResponse); return new ListResponse(r, OrganizationUserBulkResponse);
} }
deleteOrganizationUser(organizationId: string, id: string): Promise<void> {
return this.apiService.send(
"DELETE",
"/organizations/" + organizationId + "/users/" + id + "/delete-account",
null,
true,
false,
);
}
} }

View File

@@ -220,7 +220,8 @@ describe("DefaultActiveUserState", () => {
it("should not emit a previous users value if that user is no longer active", async () => { it("should not emit a previous users value if that user is no longer active", async () => {
const user1Data: Jsonify<TestState> = { const user1Data: Jsonify<TestState> = {
date: "2020-09-21T13:14:17.648Z", date: "2020-09-21T13:14:17.648Z",
array: ["value"], // NOTE: `as any` is here until we migrate to Nx: https://bitwarden.atlassian.net/browse/PM-6493
array: ["value"] as any,
}; };
const user2Data: Jsonify<TestState> = { const user2Data: Jsonify<TestState> = {
date: "2020-09-21T13:14:17.648Z", date: "2020-09-21T13:14:17.648Z",

View File

@@ -192,7 +192,8 @@ describe("KeyDefinition", () => {
expect(arrayDefinition).toBeTruthy(); expect(arrayDefinition).toBeTruthy();
expect(arrayDefinition.deserializer).toBeTruthy(); expect(arrayDefinition.deserializer).toBeTruthy();
const deserializedValue = arrayDefinition.deserializer([false, true]); // NOTE: `as any` is here until we migrate to Nx: https://bitwarden.atlassian.net/browse/PM-6493
const deserializedValue = arrayDefinition.deserializer([false, true] as any);
expect(deserializedValue).toBeTruthy(); expect(deserializedValue).toBeTruthy();
expect(deserializedValue).toHaveLength(2); expect(deserializedValue).toHaveLength(2);

View File

@@ -151,6 +151,7 @@ describe("Login DTO", () => {
password: "myPassword" as EncryptedString, password: "myPassword" as EncryptedString,
passwordRevisionDate: passwordRevisionDate.toISOString(), passwordRevisionDate: passwordRevisionDate.toISOString(),
totp: "myTotp" as EncryptedString, totp: "myTotp" as EncryptedString,
// NOTE: `as any` is here until we migrate to Nx: https://bitwarden.atlassian.net/browse/PM-6493
fido2Credentials: [ fido2Credentials: [
{ {
credentialId: "keyId" as EncryptedString, credentialId: "keyId" as EncryptedString,
@@ -167,7 +168,7 @@ describe("Login DTO", () => {
discoverable: "discoverable" as EncryptedString, discoverable: "discoverable" as EncryptedString,
creationDate: fido2CreationDate.toISOString(), creationDate: fido2CreationDate.toISOString(),
}, },
], ] as any,
}); });
expect(actual).toEqual({ expect(actual).toEqual({

View File

@@ -71,7 +71,7 @@ The content can be a button, anchor, or static container.
<bit-item> <bit-item>
<button bit-item-content type="button"> <button bit-item-content type="button">
<bit-avatar slot="start" text="Foo"></bit-avatar> <bit-avatar slot="start" text="Foo"></bit-avatar>
foo@bitwarden.com foo&#64;bitwarden.com
<span bitBadge variant="primary" slot="default-trailing">Auto-fill</span> <span bitBadge variant="primary" slot="default-trailing">Auto-fill</span>
<ng-container slot="secondary"> <ng-container slot="secondary">
<div>Bitwarden.com</div> <div>Bitwarden.com</div>

View File

@@ -94,7 +94,7 @@ export const ContentSlots: Story = {
slot="start" slot="start"
[text]="'Foo'" [text]="'Foo'"
></bit-avatar> ></bit-avatar>
foo@bitwarden.com foo&#64;bitwarden.com
<ng-container slot="secondary"> <ng-container slot="secondary">
<div>Bitwarden.com</div> <div>Bitwarden.com</div>
<div><em>locked</em></div> <div><em>locked</em></div>

11509
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,19 +35,20 @@
"libs/*" "libs/*"
], ],
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "16.2.14", "@angular-devkit/build-angular": "17.3.9",
"@angular-eslint/eslint-plugin": "16.3.1", "@angular-eslint/eslint-plugin": "17.5.3",
"@angular-eslint/eslint-plugin-template": "16.3.1", "@angular-eslint/eslint-plugin-template": "17.5.3",
"@angular-eslint/template-parser": "16.3.1", "@angular-eslint/schematics": "17.5.3",
"@angular/cli": "16.2.14", "@angular-eslint/template-parser": "17.5.3",
"@angular/compiler-cli": "16.2.12", "@angular/cli": "17.3.9",
"@angular/elements": "16.2.12", "@angular/compiler-cli": "17.3.12",
"@angular/elements": "17.3.12",
"@babel/core": "7.24.9", "@babel/core": "7.24.9",
"@babel/preset-env": "7.24.8", "@babel/preset-env": "7.24.8",
"@compodoc/compodoc": "1.1.25", "@compodoc/compodoc": "1.1.25",
"@electron/notarize": "2.5.0", "@electron/notarize": "2.5.0",
"@electron/rebuild": "3.7.0", "@electron/rebuild": "3.7.0",
"@ngtools/webpack": "16.2.14", "@ngtools/webpack": "17.3.9",
"@storybook/addon-a11y": "8.2.9", "@storybook/addon-a11y": "8.2.9",
"@storybook/addon-actions": "8.2.9", "@storybook/addon-actions": "8.2.9",
"@storybook/addon-designs": "8.0.3", "@storybook/addon-designs": "8.0.3",
@@ -58,7 +59,7 @@
"@storybook/manager-api": "8.2.9", "@storybook/manager-api": "8.2.9",
"@storybook/theming": "8.2.9", "@storybook/theming": "8.2.9",
"@types/argon2-browser": "1.18.4", "@types/argon2-browser": "1.18.4",
"@types/chrome": "0.0.272", "@types/chrome": "0.0.280",
"@types/firefox-webext-browser": "120.0.4", "@types/firefox-webext-browser": "120.0.4",
"@types/inquirer": "8.2.10", "@types/inquirer": "8.2.10",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
@@ -127,7 +128,6 @@
"prettier": "3.3.3", "prettier": "3.3.3",
"prettier-plugin-tailwindcss": "0.6.8", "prettier-plugin-tailwindcss": "0.6.8",
"process": "0.11.10", "process": "0.11.10",
"regedit": "3.0.3",
"remark-gfm": "4.0.0", "remark-gfm": "4.0.0",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"sass": "1.74.1", "sass": "1.74.1",
@@ -139,7 +139,7 @@
"ts-loader": "9.5.1", "ts-loader": "9.5.1",
"tsconfig-paths-webpack-plugin": "4.1.0", "tsconfig-paths-webpack-plugin": "4.1.0",
"type-fest": "2.19.0", "type-fest": "2.19.0",
"typescript": "5.1.6", "typescript": "5.4.2",
"url": "0.11.4", "url": "0.11.4",
"util": "0.12.5", "util": "0.12.5",
"wait-on": "8.0.1", "wait-on": "8.0.1",
@@ -149,22 +149,22 @@
"webpack-node-externals": "3.0.0" "webpack-node-externals": "3.0.0"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "16.2.12", "@angular/animations": "17.3.12",
"@angular/cdk": "16.2.14", "@angular/cdk": "17.3.10",
"@angular/common": "16.2.12", "@angular/common": "17.3.12",
"@angular/compiler": "16.2.12", "@angular/compiler": "17.3.12",
"@angular/core": "16.2.12", "@angular/core": "17.3.12",
"@angular/forms": "16.2.12", "@angular/forms": "17.3.12",
"@angular/platform-browser": "16.2.12", "@angular/platform-browser": "17.3.12",
"@angular/platform-browser-dynamic": "16.2.12", "@angular/platform-browser-dynamic": "17.3.12",
"@angular/router": "16.2.12", "@angular/router": "17.3.12",
"@bitwarden/sdk-internal": "0.1.7", "@bitwarden/sdk-internal": "0.1.7",
"@electron/fuses": "1.8.0", "@electron/fuses": "1.8.0",
"@koa/multer": "3.0.2", "@koa/multer": "3.0.2",
"@koa/router": "13.1.0", "@koa/router": "13.1.0",
"@microsoft/signalr": "8.0.7", "@microsoft/signalr": "8.0.7",
"@microsoft/signalr-protocol-msgpack": "8.0.7", "@microsoft/signalr-protocol-msgpack": "8.0.7",
"@ng-select/ng-select": "11.2.0", "@ng-select/ng-select": "12.0.7",
"argon2": "0.41.1", "argon2": "0.41.1",
"argon2-browser": "1.18.0", "argon2-browser": "1.18.0",
"big-integer": "1.6.52", "big-integer": "1.6.52",
@@ -187,8 +187,8 @@
"lowdb": "1.0.0", "lowdb": "1.0.0",
"lunr": "2.3.9", "lunr": "2.3.9",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"ngx-infinite-scroll": "16.0.0", "ngx-infinite-scroll": "17.0.1",
"ngx-toastr": "17.0.2", "ngx-toastr": "18.0.0",
"node-fetch": "2.6.12", "node-fetch": "2.6.12",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"nord": "0.2.1", "nord": "0.2.1",
@@ -202,9 +202,9 @@
"qrious": "4.0.2", "qrious": "4.0.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tabbable": "6.2.0", "tabbable": "6.2.0",
"tldts": "6.1.56", "tldts": "6.1.58",
"utf-8-validate": "6.0.4", "utf-8-validate": "6.0.4",
"zone.js": "0.13.3", "zone.js": "0.14.10",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"
}, },
"overrides": { "overrides": {