1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 01:03:35 +00:00

Merge branch 'main' into pm-13345-Add-Remove-Bitwarden-Families-policy-in-Admin-Console

This commit is contained in:
Cy Okeke
2024-11-08 10:26:51 +01:00
156 changed files with 2330 additions and 1128 deletions

View File

@@ -2878,7 +2878,7 @@
"message": "E-Mail generieren"
},
"generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$",
"message": "Wert muss zwischen $MIN$ und $MAX$ liegen",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {

View File

@@ -20,16 +20,16 @@
"message": "Crear cuenta"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "¿Nuevo en Bitwarden?"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "Iniciar sesión con clave de acceso"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "Usar inicio de sesión único"
},
"welcomeBack": {
"message": "Welcome back"
"message": "Bienvenido de nuevo"
},
"setAStrongPassword": {
"message": "Establece una contraseña fuerte"
@@ -84,7 +84,7 @@
"message": "Incorporarse a la organización"
},
"joinOrganizationName": {
"message": "Join $ORGANIZATIONNAME$",
"message": "Unirse a $ORGANIZATIONNAME$",
"placeholders": {
"organizationName": {
"content": "$1",
@@ -120,7 +120,7 @@
"message": "Copiar contraseña"
},
"copyPassphrase": {
"message": "Copy passphrase"
"message": "Copiar frase de contraseña"
},
"copyNote": {
"message": "Copiar nota"
@@ -153,7 +153,7 @@
"message": "Copiar número de licencia"
},
"copyCustomField": {
"message": "Copy $FIELD$",
"message": "Copiar $FIELD$",
"placeholders": {
"field": {
"content": "$1",
@@ -162,13 +162,13 @@
}
},
"copyWebsite": {
"message": "Copy website"
"message": "Copiar sitio web"
},
"copyNotes": {
"message": "Copy notes"
"message": "Copiar notas"
},
"fill": {
"message": "Fill",
"message": "Rellenar",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
"autoFill": {
@@ -223,13 +223,13 @@
"message": "Añadir elemento"
},
"accountEmail": {
"message": "Account email"
"message": "Correo electrónico de la cuenta"
},
"requestHint": {
"message": "Request hint"
"message": "Solicitar pista"
},
"requestPasswordHint": {
"message": "Request password hint"
"message": "Solicitar pista de la contraseña"
},
"enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": {
"message": "Enter your account email address and your password hint will be sent to you"
@@ -427,7 +427,7 @@
"message": "Generar contraseña"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "Generar frase de contraseña"
},
"regeneratePassword": {
"message": "Regenerar contraseña"
@@ -567,7 +567,7 @@
"message": "Notas"
},
"privateNote": {
"message": "Private note"
"message": "Nota privada"
},
"note": {
"message": "Nota"
@@ -624,7 +624,7 @@
"message": "Tiempo de sesión agotado"
},
"vaultTimeoutHeader": {
"message": "Vault timeout"
"message": "Tiempo de espera de la caja fuerte"
},
"otherOptions": {
"message": "Otras opciones"
@@ -645,13 +645,13 @@
"message": "Tu caja fuerte está bloqueada. Verifica tu identidad para continuar."
},
"yourVaultIsLockedV2": {
"message": "Your vault is locked"
"message": "Tu caja fuerte está bloqueada"
},
"yourAccountIsLocked": {
"message": "Your account is locked"
"message": "Tu cuenta está bloqueada"
},
"or": {
"message": "or"
"message": "o"
},
"unlock": {
"message": "Desbloquear"
@@ -676,7 +676,7 @@
"message": "Tiempo de espera de la caja fuerte"
},
"vaultTimeout1": {
"message": "Timeout"
"message": "Tiempo de espera"
},
"lockNow": {
"message": "Bloquear"
@@ -4708,11 +4708,11 @@
"description": "Represents the - key in screen reader content as a readable word"
},
"plusCharacterDescriptor": {
"message": "Plus",
"message": "s",
"description": "Represents the + key in screen reader content as a readable word"
},
"equalsCharacterDescriptor": {
"message": "Equals",
"message": "Igual",
"description": "Represents the = key in screen reader content as a readable word"
},
"braceLeftCharacterDescriptor": {
@@ -4736,15 +4736,15 @@
"description": "Represents the | key in screen reader content as a readable word"
},
"backSlashCharacterDescriptor": {
"message": "Back slash",
"message": "Contrabarra",
"description": "Represents the back slash key in screen reader content as a readable word"
},
"colonCharacterDescriptor": {
"message": "Colon",
"message": "Dos puntos",
"description": "Represents the : key in screen reader content as a readable word"
},
"semicolonCharacterDescriptor": {
"message": "Semicolon",
"message": "Punto y coma",
"description": "Represents the ; key in screen reader content as a readable word"
},
"doubleQuoteCharacterDescriptor": {

View File

@@ -1424,7 +1424,7 @@
"message": "Palvelimen URL"
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "Itse ylläpidetyn palvelimen URL-osoite",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@@ -1795,13 +1795,13 @@
"message": "Salasanahistoria"
},
"generatorHistory": {
"message": "Generator history"
"message": "Generaattorihistoria"
},
"clearGeneratorHistoryTitle": {
"message": "Clear generator history"
"message": "Tyhjennä generaattorihistoria"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
"message": "Jos jatkat, kaikki generaattorihistorian kohteet poistetaan. Haluatko varmasti jatkaa?"
},
"back": {
"message": "Takaisin"
@@ -1920,7 +1920,7 @@
"message": "Tyhjennä historia"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "Mitään näytettävää ei ole"
},
"nothingGeneratedRecently": {
"message": "Et ole luonut mitään hiljattain"
@@ -2710,7 +2710,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
"message": "out of $TOTAL$",
"message": "/$TOTAL$",
"placeholders": {
"total": {
"content": "$1",

View File

@@ -147,7 +147,7 @@
"message": "Kopiera personnummer"
},
"copyPassportNumber": {
"message": "Copy passport number"
"message": "Kopiera passnummer"
},
"copyLicenseNumber": {
"message": "Copy license number"
@@ -4624,7 +4624,7 @@
"message": "Items that have been in trash more than 30 days will automatically be deleted"
},
"restore": {
"message": "Restore"
"message": "Återställ"
},
"deleteForever": {
"message": "Delete forever"
@@ -4744,7 +4744,7 @@
"description": "Represents the : key in screen reader content as a readable word"
},
"semicolonCharacterDescriptor": {
"message": "Semicolon",
"message": "Semikolon",
"description": "Represents the ; key in screen reader content as a readable word"
},
"doubleQuoteCharacterDescriptor": {
@@ -4756,11 +4756,11 @@
"description": "Represents the ' key in screen reader content as a readable word"
},
"lessThanCharacterDescriptor": {
"message": "Less than",
"message": "Mindre än",
"description": "Represents the < key in screen reader content as a readable word"
},
"greaterThanCharacterDescriptor": {
"message": "Greater than",
"message": "Större än",
"description": "Represents the > key in screen reader content as a readable word"
},
"commaCharacterDescriptor": {
@@ -4772,7 +4772,7 @@
"description": "Represents the . key in screen reader content as a readable word"
},
"questionCharacterDescriptor": {
"message": "Question mark",
"message": "Frågetecken",
"description": "Represents the ? key in screen reader content as a readable word"
},
"forwardSlashCharacterDescriptor": {

View File

@@ -168,7 +168,7 @@
"message": "複製備註"
},
"fill": {
"message": "Fill",
"message": "填入",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
"autoFill": {
@@ -458,7 +458,7 @@
"description": "deprecated. Use specialCharactersLabel instead."
},
"include": {
"message": "Include",
"message": "包含",
"description": "Card header for password generator include block"
},
"uppercaseDescription": {
@@ -730,10 +730,10 @@
"message": "安全"
},
"confirmMasterPassword": {
"message": "Confirm master password"
"message": "確認主密碼"
},
"masterPassword": {
"message": "Master password"
"message": "主密碼"
},
"masterPassImportant": {
"message": "Your master password cannot be recovered if you forget it!"
@@ -1092,10 +1092,10 @@
"message": "This file export will be password protected and require the file password to decrypt."
},
"filePassword": {
"message": "File password"
"message": "檔案密碼"
},
"exportPasswordDescription": {
"message": "This password will be used to export and import this file"
"message": "此密碼將用於匯出和匯入此檔案"
},
"accountRestrictedOptionDescription": {
"message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account."
@@ -3542,7 +3542,7 @@
"description": "Screen reader text (aria-label) for new item button in overlay"
},
"newLogin": {
"message": "New login",
"message": "新增登入資訊",
"description": "Button text to display within inline menu when there are no matching items on a login field"
},
"addNewLoginItemAria": {

View File

@@ -9,7 +9,7 @@
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account *ngIf="showAcctSwitcher"></app-current-account>
<app-current-account *ngIf="showAcctSwitcher && hasLoggedInAccount"></app-current-account>
</ng-container>
</popup-header>

View File

@@ -15,6 +15,7 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { CurrentAccountComponent } from "../account-switching/current-account.component";
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon";
@@ -50,6 +51,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
protected pageIcon: Icon;
protected showReadonlyHostname: boolean;
protected maxWidth: "md" | "3xl";
protected hasLoggedInAccount: boolean = false;
protected theme: string;
protected logo = ExtensionBitwardenLogo;
@@ -59,6 +61,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private i18nService: I18nService,
private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private accountSwitcherService: AccountSwitcherService,
) {}
async ngOnInit(): Promise<void> {
@@ -68,6 +71,12 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
// Listen for page changes and update the page data appropriately
this.listenForPageDataChanges();
this.listenForServiceDataChanges();
this.accountSwitcherService.availableAccounts$
.pipe(takeUntil(this.destroy$))
.subscribe((accounts) => {
this.hasLoggedInAccount = accounts.some((account) => account.id !== "addAccount");
});
}
private listenForPageDataChanges() {

View File

@@ -27,6 +27,7 @@ import { ButtonModule, I18nMockService } from "@bitwarden/components";
import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon";
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service";
import {
@@ -45,6 +46,7 @@ const decorators = (options: {
applicationVersion?: string;
clientType?: ClientType;
hostName?: string;
accounts?: any[];
}) => {
return [
componentWrapperDecorator(
@@ -83,6 +85,13 @@ const decorators = (options: {
}),
},
},
{
provide: AccountSwitcherService,
useValue: {
availableAccounts$: of(options.accounts || []),
SPECIAL_ADD_ACCOUNT_ID: "addAccount",
} as Partial<AccountSwitcherService>,
},
{
provide: AuthService,
useValue: {
@@ -300,3 +309,64 @@ export const DynamicContentExample: Story = {
],
}),
};
export const HasLoggedInAccountExample: Story = {
render: (args) => ({
props: args,
template: "<router-outlet></router-outlet>",
}),
decorators: decorators({
components: [DefaultPrimaryOutletExampleComponent],
routes: [
{
path: "**",
redirectTo: "has-logged-in-account",
pathMatch: "full",
},
{
path: "",
component: ExtensionAnonLayoutWrapperComponent,
children: [
{
path: "has-logged-in-account",
data: {
hasLoggedInAccount: true,
showAcctSwitcher: true,
},
children: [
{
path: "",
component: DefaultPrimaryOutletExampleComponent,
},
{
path: "",
component: DefaultSecondaryOutletExampleComponent,
outlet: "secondary",
},
{
path: "",
component: DefaultEnvSelectorOutletExampleComponent,
outlet: "environment-selector",
},
],
},
],
},
],
accounts: [
{
name: "Test User",
email: "testuser@bitwarden.com",
id: "123e4567-e89b-12d3-a456-426614174000",
server: "bitwarden.com",
status: 2,
isActive: false,
},
{
name: "addAccount",
id: "addAccount",
isActive: false,
},
],
}),
};

View File

@@ -1,10 +1,12 @@
import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { Subject, firstValueFrom, switchMap, takeUntil } from "rxjs";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, firstValueFrom, switchMap, takeUntil, tap } from "rxjs";
import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component";
import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "@bitwarden/components";
@@ -38,9 +40,13 @@ export class HomeComponent implements OnInit, OnDestroy {
private accountSwitcherService: AccountSwitcherService,
private registerRouteService: RegisterRouteService,
private toastService: ToastService,
private configService: ConfigService,
private route: ActivatedRoute,
) {}
async ngOnInit(): Promise<void> {
this.listenForUnauthUiRefreshFlagChanges();
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
const rememberEmail = this.loginEmailService.getRememberEmail();
@@ -70,6 +76,29 @@ export class HomeComponent implements OnInit, OnDestroy {
this.destroyed$.complete();
}
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
if (flag) {
const uniqueQueryParams = {
...this.route.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(),
};
await this.router.navigate(["/login"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroyed$),
)
.subscribe();
}
get availableAccounts$() {
return this.accountSwitcherService.availableAccounts$;
}

View File

@@ -219,7 +219,11 @@ export class AutofillComponent implements OnInit {
: AutofillOverlayVisibility.Off;
await this.autofillSettingsService.setInlineMenuVisibility(newInlineMenuVisibilityValue);
await this.requestPrivacyPermission();
// No need to initiate browser permission request if a feature is being turned off
if (newInlineMenuVisibilityValue !== AutofillOverlayVisibility.Off) {
await this.requestPrivacyPermission();
}
}
async updateAutofillOnPageLoad() {

View File

@@ -58,11 +58,33 @@ export class BrowserApi {
}
static async createWindow(options: chrome.windows.CreateData): Promise<chrome.windows.Window> {
return new Promise((resolve) =>
chrome.windows.create(options, (window) => {
resolve(window);
}),
);
return new Promise((resolve) => {
chrome.windows.create(options, async (newWindow) => {
if (!BrowserApi.isSafariApi) {
return resolve(newWindow);
}
// Safari doesn't close the default extension popup when a new window is created so we need to
// manually trigger the close by focusing the main window after the new window is created
const allWindows = await new Promise<chrome.windows.Window[]>((resolve) => {
chrome.windows.getAll({ windowTypes: ["normal"] }, (windows) => resolve(windows));
});
const mainWindow = allWindows.find((window) => window.id !== newWindow.id);
// No main window found, resolve the new window
if (mainWindow == null || !mainWindow.id) {
return resolve(newWindow);
}
// Focus the main window to close the extension popup
chrome.windows.update(mainWindow.id, { focused: true }, () => {
// Refocus the newly created window
chrome.windows.update(newWindow.id, { focused: true }, () => {
resolve(newWindow);
});
});
});
});
}
/**

View File

@@ -24,9 +24,9 @@ import {
SendFormConfig,
SendFormConfigService,
SendFormMode,
SendFormModule,
} from "@bitwarden/send-ui";
import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module";
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";

View File

@@ -39,9 +39,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.86"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]]
name = "arboard"
@@ -388,9 +388,9 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.9.4"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
dependencies = [
"core-foundation-sys",
"libc",
@@ -1154,9 +1154,9 @@ dependencies = [
[[package]]
name = "napi"
version = "2.16.11"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53575dfa17f208dd1ce3a2da2da4659aae393b256a472f2738a8586a6c4107fd"
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [
"bitflags",
"ctor",
@@ -1667,9 +1667,9 @@ checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
[[package]]
name = "security-framework"
version = "2.11.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71"
dependencies = [
"bitflags",
"core-foundation",
@@ -1680,9 +1680,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.11.0"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [
"core-foundation-sys",
"libc",
@@ -1867,18 +1867,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.61"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
dependencies = [
"proc-macro2",
"quote",
@@ -2487,9 +2487,9 @@ dependencies = [
[[package]]
name = "zbus"
version = "4.3.1"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "851238c133804e0aa888edf4a0229481c753544ca12a60fd1c3230c8a500fe40"
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
dependencies = [
"async-broadcast",
"async-executor",
@@ -2525,9 +2525,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
version = "4.3.1"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d5a3f12c20bd473be3194af6b49d50d7bb804ef3192dc70eddedb26b85d9da7"
checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -2583,9 +2583,9 @@ dependencies = [
[[package]]
name = "zvariant"
version = "4.1.2"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1724a2b330760dc7d2a8402d841119dc869ef120b139d29862d6980e9c75bfc9"
checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe"
dependencies = [
"endi",
"enumflags2",
@@ -2596,9 +2596,9 @@ dependencies = [
[[package]]
name = "zvariant_derive"
version = "4.1.2"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55025a7a518ad14518fb243559c058a2e5b848b015e31f1d90414f36e3317859"
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@@ -2609,9 +2609,9 @@ dependencies = [
[[package]]
name = "zvariant_utils"
version = "2.0.0"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc242db087efc22bd9ade7aa7809e4ba828132edc312871584a6b4391bdf8786"
checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -23,7 +23,7 @@ sys = [
[dependencies]
aes = "=0.8.4"
anyhow = "=1.0.86"
anyhow = "=1.0.93"
arboard = { version = "=3.4.1", default-features = false, features = [
"wayland-data-control",
] }
@@ -38,7 +38,7 @@ rand = "=0.8.5"
retry = "=2.0.0"
scopeguard = "=1.2.0"
sha2 = "=0.10.8"
thiserror = "=1.0.61"
thiserror = "=1.0.68"
tokio = { version = "=1.41.0", features = ["io-util", "sync", "macros"] }
tokio-util = "=0.7.12"
typenum = "=1.17.0"
@@ -61,12 +61,12 @@ windows = { version = "=0.57.0", features = [
keytar = "=0.1.6"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = { version = "=0.9.4", optional = true }
security-framework = { version = "=2.11.0", optional = true }
security-framework-sys = { version = "=2.11.0", optional = true }
core-foundation = { version = "=0.10.0", optional = true }
security-framework = { version = "=3.0.0", optional = true }
security-framework-sys = { version = "=2.12.0", optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
gio = { version = "=0.19.5", optional = true }
libsecret = { version = "=0.5.0", optional = true }
zbus = { version = "=4.3.1", optional = true }
zbus = { version = "=4.4.0", optional = true }
zbus_polkit = { version = "=4.0.0", optional = true }

View File

@@ -14,9 +14,9 @@ default = []
manual_test = []
[dependencies]
anyhow = "=1.0.86"
anyhow = "=1.0.93"
desktop_core = { path = "../core" }
napi = { version = "=2.16.11", features = ["async"] }
napi = { version = "=2.16.13", features = ["async"] }
napi-derive = "=2.16.12"
tokio = { version = "1.38.0" }
tokio-util = "0.7.11"

View File

@@ -7,7 +7,7 @@ version = "0.0.0"
publish = false
[dependencies]
anyhow = "=1.0.86"
anyhow = "=1.0.93"
desktop_core = { path = "../core", default-features = false }
futures = "0.3.30"
log = "0.4.22"

View File

@@ -58,30 +58,46 @@ async function run(context) {
id = identities[0].id;
}
console.log(`Signing proxy binary before the main bundle, using identity '${id}'`);
console.log(
`Signing proxy binary before the main bundle, using identity '${id}', for build ${context.electronPlatformName}`,
);
const appName = context.packager.appInfo.productFilename;
const appPath = `${context.appOutDir}/${appName}.app`;
const proxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy");
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
const packageId = "com.bitwarden.desktop";
const entitlementsName = "entitlements.desktop_proxy.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
const inheritProxyPath = path.join(appPath, "Contents", "MacOS", "desktop_proxy.inherit");
const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist";
const inheritEntitlementsPath = path.join(
__dirname,
"..",
"resources",
inheritEntitlementsName,
);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`,
);
if (is_mas) {
const entitlementsName = "entitlements.desktop_proxy.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
const inheritEntitlementsName = "entitlements.desktop_proxy.inherit.plist";
const inheritEntitlementsPath = path.join(
__dirname,
"..",
"resources",
inheritEntitlementsName,
);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${inheritEntitlementsPath} ${inheritProxyPath}`,
);
} else {
// For non-Appstore builds, we don't need the inherit binary as they are not sandboxed,
// but we sign and include it anyway for consistency. It should be removed once DDG supports the proxy directly.
const entitlementsName = "entitlements.mac.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${inheritProxyPath}`,
);
}
}
}

View File

@@ -1,7 +1,7 @@
import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { Subject, takeUntil, tap } from "rxjs";
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
@@ -14,8 +14,10 @@ import {
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -76,6 +78,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
webAuthnLoginService: WebAuthnLoginServiceAbstraction,
registerRouteService: RegisterRouteService,
toastService: ToastService,
private configService: ConfigService,
) {
super(
devicesApiService,
@@ -105,6 +108,8 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
}
async ngOnInit() {
this.listenForUnauthUiRefreshFlagChanges();
await super.ngOnInit();
await this.getLoginWithDevice(this.loggedEmail);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
@@ -137,6 +142,29 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
this.componentDestroyed$.complete();
}
private listenForUnauthUiRefreshFlagChanges() {
this.configService
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
.pipe(
tap(async (flag) => {
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
if (flag) {
const uniqueQueryParams = {
...this.route.queryParams,
// adding a unique timestamp to the query params to force a reload
t: new Date().getTime().toString(),
};
await this.router.navigate(["/"], {
queryParams: uniqueQueryParams,
});
}
}),
takeUntil(this.destroy$),
)
.subscribe();
}
async settings() {
const [modal, childComponent] = await this.modalService.openViewRef(
EnvironmentComponent,

View File

@@ -1684,10 +1684,10 @@
"message": "Die Kontolöschung ist dauerhaft. Sie kann nicht rückgängig gemacht werden."
},
"cannotDeleteAccount": {
"message": "Cannot delete account"
"message": "Konto kann nicht gelöscht werden"
},
"cannotDeleteAccountDesc": {
"message": "This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details."
"message": "Diese Aktion kann nicht abgeschlossen werden, da dein Konto im Besitz einer Organisation ist. Kontaktiere deinen Organisationsadministrator für weitere Details."
},
"accountDeleted": {
"message": "Konto gelöscht"
@@ -2394,7 +2394,7 @@
"message": "E-Mail generieren"
},
"generatorBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$",
"message": "Wert muss zwischen $MIN$ und $MAX$ liegen",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {

View File

@@ -1800,7 +1800,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
"message": "out of $TOTAL$",
"message": "/$TOTAL$",
"placeholders": {
"total": {
"content": "$1",

View File

@@ -40,9 +40,9 @@
></bit-nav-item>
</bit-nav-group>
<bit-nav-item
*ngIf="isAccessIntelligenceFeatureEnabled"
[text]="'accessIntelligence' | i18n"
route="access-intelligence"
*ngIf="isRiskInsightsFeatureEnabled"
[text]="'riskInsights' | i18n"
route="risk-insights"
></bit-nav-item>
<bit-nav-group
icon="bwi-billing"

View File

@@ -51,7 +51,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
showPaymentAndHistory$: Observable<boolean>;
hideNewOrgButton$: Observable<boolean>;
organizationIsUnmanaged$: Observable<boolean>;
isAccessIntelligenceFeatureEnabled = false;
isRiskInsightsFeatureEnabled = false;
private _destroy = new Subject<void>();
@@ -71,7 +71,7 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
async ngOnInit() {
document.body.classList.remove("layout_frontend");
this.isAccessIntelligenceFeatureEnabled = await this.configService.getFeatureFlag(
this.isRiskInsightsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.AccessIntelligence,
);

View File

@@ -63,10 +63,10 @@ const routes: Routes = [
),
},
{
path: "access-intelligence",
path: "risk-insights",
loadChildren: () =>
import("../../tools/access-intelligence/access-intelligence.module").then(
(m) => m.AccessIntelligenceModule,
import("../../tools/risk-insights/risk-insights.module").then(
(m) => m.RiskInsightsModule,
),
},
{

View File

@@ -123,20 +123,22 @@ export class AccountComponent implements OnInit, OnDestroy {
this.canEditSubscription = organization.canEditSubscription;
this.canUseApi = organization.useApi;
// Update disabled states - reactive forms prefers not using disabled attribute
// Disabling these fields for self hosted orgs is deprecated
// This block can be completely removed as part of
// https://bitwarden.atlassian.net/browse/PM-10863
if (!this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) {
if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable();
this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable();
}
}
if (!this.selfHosted && this.canEditSubscription) {
this.formGroup.get("billingEmail").enable();
// Update disabled states - reactive forms prefers not using disabled attribute
if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
if (this.canEditSubscription) {
this.formGroup.get("billingEmail").enable();
}
}
// Org Response

View File

@@ -48,16 +48,7 @@
}}</span>
</dd>
<dt>{{ "nextCharge" | i18n }}</dt>
<dd *ngIf="!enableTimeThreshold">
{{
nextInvoice
? (nextInvoice.date | date: "mediumDate") +
", " +
(nextInvoice.amount | currency: "$")
: "-"
}}
</dd>
<dd *ngIf="enableTimeThreshold">
<dd>
{{
nextInvoice
? (sub.subscription.periodEndDate | date: "mediumDate") +

View File

@@ -38,13 +38,9 @@ export class UserSubscriptionComponent implements OnInit {
sub: SubscriptionResponse;
selfHosted = false;
cloudWebVaultUrl: string;
enableTimeThreshold: boolean;
cancelPromise: Promise<any>;
reinstatePromise: Promise<any>;
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableTimeThreshold,
);
protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
@@ -69,7 +65,6 @@ export class UserSubscriptionComponent implements OnInit {
async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
await this.load();
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
this.firstLoaded = true;
}

View File

@@ -48,10 +48,7 @@
<dt [ngClass]="{ 'tw-text-danger': isExpired }">
{{ "subscriptionExpiration" | i18n }}
</dt>
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="!enableTimeThreshold">
{{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
</dd>
<dd [ngClass]="{ 'tw-text-danger': isExpired }" *ngIf="enableTimeThreshold">
<dd [ngClass]="{ 'tw-text-danger': isExpired }">
{{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }}
</dd>
</ng-container>

View File

@@ -52,7 +52,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
loading = true;
locale: string;
showUpdatedSubscriptionStatusSection$: Observable<boolean>;
enableTimeThreshold: boolean;
preSelectedProductTier: ProductTierType = ProductTierType.Free;
showSubscription = true;
showSelfHost = false;
@@ -65,10 +64,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
FeatureFlag.EnableConsolidatedBilling,
);
protected enableTimeThreshold$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableTimeThreshold,
);
protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$(
FeatureFlag.EnableUpgradePasswordManagerSub,
);
@@ -117,7 +112,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$(
FeatureFlag.AC1795_UpdatedSubscriptionStatusSection,
);
this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$);
}
ngOnDestroy() {
@@ -298,9 +292,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
return this.i18nService.t("subscriptionUpgrade", this.sub.seats.toString());
}
} else if (this.sub.maxAutoscaleSeats === this.sub.seats && this.sub.seats != null) {
if (!this.enableTimeThreshold) {
return this.i18nService.t("subscriptionMaxReached", this.sub.seats.toString());
}
const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage";
@@ -311,21 +302,11 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
} else if (this.userOrg.productTierType === ProductTierType.TeamsStarter) {
return this.i18nService.t("subscriptionUserSeatsWithoutAdditionalSeatsOption", 10);
} else if (this.sub.maxAutoscaleSeats == null) {
if (!this.enableTimeThreshold) {
return this.i18nService.t("subscriptionUserSeatsUnlimitedAutoscale");
}
const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage";
return this.i18nService.t(seatAdjustmentMessage);
} else {
if (!this.enableTimeThreshold) {
return this.i18nService.t(
"subscriptionUserSeatsLimitedAutoscale",
this.sub.maxAutoscaleSeats.toString(),
);
}
const seatAdjustmentMessage = this.sub.plan.isAnnual
? "annualSubscriptionUserSeatsMessage"
: "monthlySubscriptionUserSeatsMessage";

View File

@@ -1,9 +0,0 @@
import { NgModule } from "@angular/core";
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
import { AccessIntelligenceComponent } from "./access-intelligence.component";
@NgModule({
imports: [AccessIntelligenceComponent, AccessIntelligenceRoutingModule],
})
export class AccessIntelligenceModule {}

View File

@@ -1,120 +0,0 @@
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="totalMembersMap.size - 3"
[maxValue]="totalMembersMap.size"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="totalMembersMap.size - 1"
[maxValue]="totalMembersMap.size"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
<button class="tw-rounded-lg" type="button" buttonType="secondary" bitButton>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="totalMembersMap.size - 3"
[maxValue]="totalMembersMap.size"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="totalMembersMap.size - 1"
[maxValue]="totalMembersMap.size"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search class="tw-grow" [formControl]="searchControl"></bit-search>
<button
class="tw-rounded-lg"
type="button"
buttonType="secondary"
[disabled]="!selectedIds.size"
bitButton
[bitAction]="markAppsAsCritical"
appA11yTitle="{{ 'markAppAsCritical' | i18n }}"
>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td bitCell>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>
</div>

View File

@@ -57,6 +57,7 @@
type="button"
buttonType="secondary"
bitButton
*ngIf="isCritialAppsFeatureEnabled"
[disabled]="!selectedIds.size"
[loading]="markingAsCritical"
(click)="markAppsAsCritical()"
@@ -68,7 +69,7 @@
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th></th>
<th *ngIf="isCritialAppsFeatureEnabled"></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
@@ -78,7 +79,7 @@
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td>
<td *ngIf="isCritialAppsFeatureEnabled">
<input
bitCheckbox
type="checkbox"

View File

@@ -7,6 +7,8 @@ import { debounceTime, firstValueFrom, map } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -41,6 +43,7 @@ export class AllApplicationsComponent implements OnInit {
protected organization: Organization;
noItemsIcon = Icons.Security;
protected markingAsCritical = false;
isCritialAppsFeatureEnabled = false;
// MOCK DATA
protected mockData = applicationTableMockData;
@@ -49,7 +52,7 @@ export class AllApplicationsComponent implements OnInit {
protected mockTotalMembersCount = 0;
protected mockTotalAppsCount = 0;
ngOnInit() {
async ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
@@ -60,6 +63,10 @@ export class AllApplicationsComponent implements OnInit {
}),
)
.subscribe();
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
}
constructor(
@@ -70,6 +77,7 @@ export class AllApplicationsComponent implements OnInit {
protected activatedRoute: ActivatedRoute,
protected toastService: ToastService,
protected organizationService: OrganizationService,
protected configService: ConfigService,
) {
this.dataSource.data = applicationTableMockData;
this.searchControl.valueChanges

View File

@@ -12,8 +12,8 @@ import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { AccessIntelligenceTabType } from "./access-intelligence.component";
import { applicationTableMockData } from "./application-table.mock";
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({
standalone: true,
@@ -49,8 +49,8 @@ export class CriticalApplicationsComponent implements OnInit {
}
goToAllAppsTab = async () => {
await this.router.navigate([`organizations/${this.organizationId}/access-intelligence`], {
queryParams: { tabIndex: AccessIntelligenceTabType.AllApps },
await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], {
queryParams: { tabIndex: RiskInsightsTabType.AllApps },
queryParamsHandling: "merge",
});
};

View File

@@ -4,7 +4,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -6,7 +6,7 @@ import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
// eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";

View File

@@ -0,0 +1,64 @@
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td bitCell>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@@ -5,7 +5,7 @@ import { ActivatedRoute } from "@angular/router";
import { debounceTime, map } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

View File

@@ -4,7 +4,7 @@ import { mock } from "jest-mock-extended";
import { of } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

View File

@@ -6,7 +6,7 @@ import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
// eslint-disable-next-line no-restricted-imports
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/access-intelligence";
import { PasswordHealthService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";

View File

@@ -4,15 +4,15 @@ import { RouterModule, Routes } from "@angular/router";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccessIntelligenceComponent } from "./access-intelligence.component";
import { RiskInsightsComponent } from "./risk-insights.component";
const routes: Routes = [
{
path: "",
component: AccessIntelligenceComponent,
component: RiskInsightsComponent,
canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)],
data: {
titleId: "accessIntelligence",
titleId: "RiskInsights",
},
},
];
@@ -21,4 +21,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AccessIntelligenceRoutingModule {}
export class RiskInsightsRoutingModule {}

View File

@@ -1,4 +1,4 @@
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "accessIntelligence" | i18n }}</div>
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "riskInsights" | i18n }}</div>
<h1 bitTypography="h1">{{ "passwordRisk" | i18n }}</h1>
<div class="tw-text-muted">{{ "discoverAtRiskPasswords" | i18n }}</div>
<div class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-2 tw-my-4">
@@ -19,7 +19,7 @@
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}">
<tools-all-applications></tools-all-applications>
</bit-tab>
<bit-tab>
<bit-tab *ngIf="isCritialAppsFeatureEnabled">
<ng-template bitTabLabel>
<i class="bwi bwi-star"></i>
{{ "criticalApplicationsWithCount" | i18n: criticalApps.length }}

View File

@@ -1,9 +1,11 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
import { HeaderModule } from "../../layouts/header/header.module";
@@ -15,7 +17,7 @@ import { PasswordHealthMembersURIComponent } from "./password-health-members-uri
import { PasswordHealthMembersComponent } from "./password-health-members.component";
import { PasswordHealthComponent } from "./password-health.component";
export enum AccessIntelligenceTabType {
export enum RiskInsightsTabType {
AllApps = 0,
CriticalApps = 1,
NotifiedMembers = 2,
@@ -23,7 +25,7 @@ export enum AccessIntelligenceTabType {
@Component({
standalone: true,
templateUrl: "./access-intelligence.component.html",
templateUrl: "./risk-insights.component.html",
imports: [
AllApplicationsComponent,
AsyncActionsModule,
@@ -39,9 +41,10 @@ export enum AccessIntelligenceTabType {
TabsModule,
],
})
export class AccessIntelligenceComponent {
tabIndex: AccessIntelligenceTabType;
export class RiskInsightsComponent implements OnInit {
tabIndex: RiskInsightsTabType;
dataLastUpdated = new Date();
isCritialAppsFeatureEnabled = false;
apps: any[] = [];
criticalApps: any[] = [];
@@ -65,12 +68,19 @@ export class AccessIntelligenceComponent {
});
};
async ngOnInit() {
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
}
constructor(
protected route: ActivatedRoute,
private router: Router,
private configService: ConfigService,
) {
route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
this.tabIndex = !isNaN(tabIndex) ? tabIndex : AccessIntelligenceTabType.AllApps;
this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps;
});
}
}

View File

@@ -0,0 +1,9 @@
import { NgModule } from "@angular/core";
import { RiskInsightsRoutingModule } from "./risk-insights-routing.module";
import { RiskInsightsComponent } from "./risk-insights.component";
@NgModule({
imports: [RiskInsightsComponent, RiskInsightsRoutingModule],
})
export class RiskInsightsModule {}

View File

@@ -7,6 +7,7 @@
*ngIf="showCipherView"
[cipher]="cipher"
[collections]="collections"
[isAdminConsole]="formConfig.isAdminConsole"
></app-cipher-view>
<vault-cipher-form
*ngIf="loadForm"

View File

@@ -6,6 +6,7 @@ import { firstValueFrom, Observable, Subject } from "rxjs";
import { map } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
@@ -17,6 +18,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import {
@@ -231,6 +234,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
private billingAccountProfileStateService: BillingAccountProfileStateService,
private premiumUpgradeService: PremiumUpgradePromptService,
private cipherAuthorizationService: CipherAuthorizationService,
private apiService: ApiService,
) {
this.updateTitle();
}
@@ -278,7 +282,20 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
if (this._originalFormMode === "add" || this._originalFormMode === "clone") {
this.formConfig.mode = "edit";
}
this.formConfig.originalCipher = await this.cipherService.get(cipherView.id);
let cipher: Cipher;
// When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint
if (this.formConfig.isAdminConsole) {
const cipherResponse = await this.apiService.getCipherAdmin(cipherView.id);
const cipherData = new CipherData(cipherResponse);
cipher = new Cipher(cipherData);
} else {
cipher = await this.cipherService.get(cipherView.id);
}
// Store the updated cipher so any following edits use the most up to date cipher
this.formConfig.originalCipher = cipher;
this._cipherModified = true;
await this.changeMode("view");
}

View File

@@ -16,13 +16,39 @@
"all" | i18n
}}</label>
</th>
<th bitCell [class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'">{{ "name" | i18n }}</th>
<!-- Organization vault -->
<th
*ngIf="showAdminActions"
bitCell
bitSortable="name"
[fn]="sortByName"
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
>
{{ "name" | i18n }}
</th>
<!-- Individual vault -->
<th
*ngIf="!showAdminActions"
bitCell
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
>
{{ "name" | i18n }}
</th>
<th bitCell *ngIf="showOwner" class="tw-hidden tw-w-2/5 lg:tw-table-cell">
{{ "owner" | i18n }}
</th>
<th bitCell class="tw-w-2/5" *ngIf="showCollections">{{ "collections" | i18n }}</th>
<th bitCell class="tw-w-2/5" *ngIf="showGroups">{{ "groups" | i18n }}</th>
<th bitCell class="tw-w-2/5" *ngIf="showPermissionsColumn">
<th bitCell bitSortable="groups" [fn]="sortByGroups" class="tw-w-2/5" *ngIf="showGroups">
{{ "groups" | i18n }}
</th>
<th
bitCell
bitSortable="permissions"
default="desc"
[fn]="sortByPermissions"
class="tw-w-2/5"
*ngIf="showPermissionsColumn"
>
{{ "permission" | i18n }}
</th>
<th bitCell class="tw-w-12 tw-text-right">

View File

@@ -1,13 +1,17 @@
import { SelectionModel } from "@angular/cdk/collections";
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CollectionView, Unassigned } from "@bitwarden/admin-console/common";
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { TableDataSource } from "@bitwarden/components";
import { SortDirection, TableDataSource } from "@bitwarden/components";
import { GroupView } from "../../../admin-console/organizations/core";
import {
CollectionPermission,
convertToPermission,
} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
import { VaultItem } from "./vault-item";
import { VaultItemEvent } from "./vault-item-event";
@@ -17,6 +21,8 @@ export const RowHeightClass = `tw-h-[65px]`;
const MaxSelectionCount = 500;
type ItemPermission = CollectionPermission | "NoAccess";
@Component({
selector: "app-vault-items",
templateUrl: "vault-items.component.html",
@@ -333,6 +339,119 @@ export class VaultItemsComponent {
return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected;
}
/**
* Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name.
*/
protected sortByName = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
// Collections before ciphers
const collectionCompare = this.prioritizeCollections(a, b, direction);
if (collectionCompare !== 0) {
return collectionCompare;
}
return this.compareNames(a, b);
};
/**
* Sorts VaultItems based on group names
*/
protected sortByGroups = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
if (
!(a.collection instanceof CollectionAdminView) &&
!(b.collection instanceof CollectionAdminView)
) {
return 0;
}
const getFirstGroupName = (collection: CollectionAdminView): string => {
if (collection.groups.length > 0) {
return collection.groups.map((group) => this.getGroupName(group.id) || "").sort()[0];
}
return null;
};
// Collections before ciphers
const collectionCompare = this.prioritizeCollections(a, b, direction);
if (collectionCompare !== 0) {
return collectionCompare;
}
const aGroupName = getFirstGroupName(a.collection as CollectionAdminView);
const bGroupName = getFirstGroupName(b.collection as CollectionAdminView);
// Collections with groups come before collections without groups.
// If a collection has no groups, getFirstGroupName returns null.
if (aGroupName === null) {
return 1;
}
if (bGroupName === null) {
return -1;
}
return aGroupName.localeCompare(bGroupName);
};
/**
* Sorts VaultItems based on their permissions, with higher permissions taking precedence.
* If permissions are equal, it falls back to sorting by name.
*/
protected sortByPermissions = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
const getPermissionPriority = (item: VaultItem): number => {
const permission = item.collection
? this.getCollectionPermission(item.collection)
: this.getCipherPermission(item.cipher);
const priorityMap = {
[CollectionPermission.Manage]: 5,
[CollectionPermission.Edit]: 4,
[CollectionPermission.EditExceptPass]: 3,
[CollectionPermission.View]: 2,
[CollectionPermission.ViewExceptPass]: 1,
NoAccess: 0,
};
return priorityMap[permission] ?? -1;
};
// Collections before ciphers
const collectionCompare = this.prioritizeCollections(a, b, direction);
if (collectionCompare !== 0) {
return collectionCompare;
}
const priorityA = getPermissionPriority(a);
const priorityB = getPermissionPriority(b);
// Higher priority first
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return this.compareNames(a, b);
};
private compareNames(a: VaultItem, b: VaultItem): number {
const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name;
return getName(a).localeCompare(getName(b));
}
/**
* Sorts VaultItems by prioritizing collections over ciphers.
* Collections are always placed before ciphers, regardless of the sorting direction.
*/
private prioritizeCollections(a: VaultItem, b: VaultItem, direction: SortDirection): number {
if (a.collection && !b.collection) {
return direction === "asc" ? -1 : 1;
}
if (!a.collection && b.collection) {
return direction === "asc" ? 1 : -1;
}
return 0;
}
private hasPersonalItems(): boolean {
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
}
@@ -346,4 +465,58 @@ export class VaultItemsComponent {
private getUniqueOrganizationIds(): Set<string> {
return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? []));
}
private getGroupName(groupId: string): string | undefined {
return this.allGroups.find((g) => g.id === groupId)?.name;
}
private getCollectionPermission(collection: CollectionView): ItemPermission {
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
if (collection.id == Unassigned && organization?.canEditUnassignedCiphers) {
return CollectionPermission.Edit;
}
if (collection.assigned) {
return convertToPermission(collection);
}
return "NoAccess";
}
private getCipherPermission(cipher: CipherView): ItemPermission {
if (!cipher.organizationId || cipher.collectionIds.length === 0) {
return CollectionPermission.Manage;
}
const filteredCollections = this.allCollections?.filter((collection) => {
if (collection.assigned) {
return cipher.collectionIds.find((id) => {
if (collection.id === id) {
return collection;
}
});
}
});
if (filteredCollections?.length === 1) {
return convertToPermission(filteredCollections[0]);
}
if (filteredCollections?.length > 0) {
const permissions = filteredCollections.map((collection) => convertToPermission(collection));
const orderedPermissions = [
CollectionPermission.Manage,
CollectionPermission.Edit,
CollectionPermission.EditExceptPass,
CollectionPermission.View,
CollectionPermission.ViewExceptPass,
];
return orderedPermissions.find((perm) => permissions.includes(perm));
}
return "NoAccess";
}
}

View File

@@ -1,5 +1,5 @@
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
import { combineLatest, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs";
import {
OrganizationUserApiService,
@@ -8,11 +8,14 @@ import {
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -53,6 +56,8 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
private resetPasswordService: OrganizationUserResetPasswordService,
private userVerificationService: UserVerificationService,
private toastService: ToastService,
private configService: ConfigService,
private organizationService: OrganizationService,
) {}
async ngOnInit() {
@@ -60,23 +65,39 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)),
);
const managingOrg$ = this.configService
.getFeatureFlag$(FeatureFlag.AccountDeprovisioning)
.pipe(
switchMap((isAccountDeprovisioningEnabled) =>
isAccountDeprovisioningEnabled
? this.organizationService.organizations$.pipe(
map((organizations) =>
organizations.find((o) => o.userIsManagedByOrganization === true),
),
)
: of(null),
),
);
combineLatest([
this.organization$,
resetPasswordPolicies$,
this.userDecryptionOptionsService.userDecryptionOptions$,
managingOrg$,
])
.pipe(takeUntil(this.destroy$))
.subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => {
.subscribe(([organization, resetPasswordPolicies, decryptionOptions, managingOrg]) => {
this.organization = organization;
this.resetPasswordPolicy = resetPasswordPolicies.find(
(p) => p.organizationId === organization.id,
);
// A user can leave an organization if they are NOT using TDE and Key Connector, or they have a master password.
// A user can leave an organization if they are NOT a managed user and they are NOT using TDE and Key Connector, or they have a master password.
this.showLeaveOrgOption =
(decryptionOptions.trustedDeviceOption == undefined &&
managingOrg?.id !== organization.id &&
((decryptionOptions.trustedDeviceOption == undefined &&
decryptionOptions.keyConnectorOption == undefined) ||
decryptionOptions.hasMasterPassword;
decryptionOptions.hasMasterPassword);
// Hide the 3 dot menu if the user has no available actions
this.hideMenu =

View File

@@ -1,14 +1,13 @@
import { TestBed } from "@angular/core/testing";
import { BehaviorSubject } from "rxjs";
import { CollectionAdminService } from "@bitwarden/admin-console/common";
import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/services/routed-vault-filter.service";
@@ -35,27 +34,41 @@ describe("AdminConsoleCipherFormConfigService", () => {
status: OrganizationUserStatusType.Confirmed,
};
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true);
const collection = {
id: "12345-5555",
organizationId: "234534-34334",
name: "Test Collection 1",
assigned: false,
readOnly: true,
} as CollectionAdminView;
const collection2 = {
id: "12345-6666",
organizationId: "22222-2222",
name: "Test Collection 2",
assigned: true,
readOnly: false,
} as CollectionAdminView;
const organization$ = new BehaviorSubject<Organization>(testOrg as Organization);
const organizations$ = new BehaviorSubject<Organization[]>([testOrg, testOrg2] as Organization[]);
const getCipherAdmin = jest.fn().mockResolvedValue(null);
const getCipher = jest.fn().mockResolvedValue(null);
beforeEach(async () => {
getCipherAdmin.mockClear();
getCipher.mockClear();
getCipher.mockResolvedValue({ id: cipherId, name: "Test Cipher - (non-admin)" });
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
await TestBed.configureTestingModule({
providers: [
AdminConsoleCipherFormConfigService,
{ provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } },
{
provide: CollectionAdminService,
useValue: { getAll: () => Promise.resolve([collection, collection2]) },
},
{
provide: PolicyService,
useValue: { policyAppliesToActiveUser$: () => policyAppliesToActiveUser$ },
},
{ provide: OrganizationService, useValue: { get$: () => organization$, organizations$ } },
{ provide: CipherService, useValue: { get: getCipher } },
{ provide: CollectionAdminService, useValue: { getAll: () => Promise.resolve([]) } },
{
provide: RoutedVaultFilterService,
useValue: { filter$: new BehaviorSubject({ organizationId: testOrg.id }) },
@@ -86,6 +99,12 @@ describe("AdminConsoleCipherFormConfigService", () => {
expect(mode).toBe("edit");
});
it("returns all collections", async () => {
const { collections } = await adminConsoleConfigService.buildConfig("edit", cipherId);
expect(collections).toEqual([collection, collection2]);
});
it("sets admin flag based on `canEditAllCiphers`", async () => {
// Disable edit all ciphers on org
testOrg.canEditAllCiphers = false;
@@ -153,33 +172,14 @@ describe("AdminConsoleCipherFormConfigService", () => {
expect(result.organizations).toEqual([testOrg, testOrg2]);
});
describe("getCipher", () => {
it("retrieves the cipher from the cipher service", async () => {
testOrg.canEditAllCiphers = false;
it("retrieves the cipher from the admin service", async () => {
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
const result = await adminConsoleConfigService.buildConfig("clone", cipherId);
await adminConsoleConfigService.buildConfig("add", cipherId);
expect(getCipher).toHaveBeenCalledWith(cipherId);
expect(result.originalCipher.name).toBe("Test Cipher - (non-admin)");
// Admin service not needed when cipher service can return the cipher
expect(getCipherAdmin).not.toHaveBeenCalled();
});
it("retrieves the cipher from the admin service", async () => {
getCipher.mockResolvedValueOnce(null);
getCipherAdmin.mockResolvedValue({ id: cipherId, name: "Test Cipher - (admin)" });
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
await adminConsoleConfigService.buildConfig("add", cipherId);
expect(getCipherAdmin).toHaveBeenCalledWith(cipherId);
expect(getCipher).toHaveBeenCalledWith(cipherId);
});
expect(getCipherAdmin).toHaveBeenCalledWith(cipherId);
});
});
});

View File

@@ -6,9 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType, OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CipherId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@@ -25,7 +23,6 @@ import { RoutedVaultFilterService } from "../../individual-vault/vault-filter/se
export class AdminConsoleCipherFormConfigService implements CipherFormConfigService {
private policyService: PolicyService = inject(PolicyService);
private organizationService: OrganizationService = inject(OrganizationService);
private cipherService: CipherService = inject(CipherService);
private routedVaultFilterService: RoutedVaultFilterService = inject(RoutedVaultFilterService);
private collectionAdminService: CollectionAdminService = inject(CollectionAdminService);
private apiService: ApiService = inject(ApiService);
@@ -51,20 +48,8 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
map(([orgs, orgId]) => orgs.find((o) => o.id === orgId)),
);
private editableCollections$ = this.organization$.pipe(
switchMap(async (org) => {
if (!org) {
return [];
}
const collections = await this.collectionAdminService.getAll(org.id);
// Users that can edit all ciphers can implicitly add to / edit within any collection
if (org.canEditAllCiphers) {
return collections;
}
// The user is only allowed to add/edit items to assigned collections that are not readonly
return collections.filter((c) => c.assigned && !c.readOnly);
}),
private allCollections$ = this.organization$.pipe(
switchMap(async (org) => await this.collectionAdminService.getAll(org.id)),
);
async buildConfig(
@@ -72,21 +57,17 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
cipherId?: CipherId,
cipherType?: CipherType,
): Promise<CipherFormConfig> {
const cipher = await this.getCipher(cipherId);
const [organization, allowPersonalOwnership, allOrganizations, allCollections] =
await firstValueFrom(
combineLatest([
this.organization$,
this.allowPersonalOwnership$,
this.allOrganizations$,
this.editableCollections$,
this.allCollections$,
]),
);
const cipher = await this.getCipher(organization, cipherId);
const collections = allCollections.filter(
(c) => c.organizationId === organization.id && c.assigned && !c.readOnly,
);
// When cloning from within the Admin Console, all organizations should be available.
// Otherwise only the one in context should be
const organizations = mode === "clone" ? allOrganizations : [organization];
@@ -100,7 +81,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
admin: organization.canEditAllCiphers ?? false,
allowPersonalOwnership: allowPersonalOwnershipOnlyForClone,
originalCipher: cipher,
collections,
collections: allCollections,
organizations,
folders: [], // folders not applicable in the admin console
hideIndividualVaultFields: true,
@@ -108,19 +89,11 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
};
}
private async getCipher(organization: Organization, id?: CipherId): Promise<Cipher | null> {
private async getCipher(id?: CipherId): Promise<Cipher | null> {
if (id == null) {
return Promise.resolve(null);
}
// Check to see if the user has direct access to the cipher
const cipherFromCipherService = await this.cipherService.get(id);
// If the organization doesn't allow admin/owners to edit all ciphers return the cipher
if (!organization.canEditAllCiphers && cipherFromCipherService != null) {
return cipherFromCipherService;
}
// Retrieve the cipher through the means of an admin
const cipherResponse = await this.apiService.getCipherAdmin(id);
cipherResponse.edit = true;

View File

@@ -5,8 +5,8 @@
"criticalApplications": {
"message": "Critical applications"
},
"accessIntelligence": {
"message": "Access Intelligence"
"riskInsights": {
"message": "Risk Insights"
},
"passwordRisk": {
"message": "Password Risk"