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:
@@ -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": {
|
||||
|
||||
@@ -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": "Má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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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$;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||
48
apps/desktop/desktop_native/Cargo.lock
generated
48
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") +
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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",
|
||||
});
|
||||
};
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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 {}
|
||||
@@ -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 }}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -7,6 +7,7 @@
|
||||
*ngIf="showCipherView"
|
||||
[cipher]="cipher"
|
||||
[collections]="collections"
|
||||
[isAdminConsole]="formConfig.isAdminConsole"
|
||||
></app-cipher-view>
|
||||
<vault-cipher-form
|
||||
*ngIf="loadForm"
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"criticalApplications": {
|
||||
"message": "Critical applications"
|
||||
},
|
||||
"accessIntelligence": {
|
||||
"message": "Access Intelligence"
|
||||
"riskInsights": {
|
||||
"message": "Risk Insights"
|
||||
},
|
||||
"passwordRisk": {
|
||||
"message": "Password Risk"
|
||||
|
||||
Reference in New Issue
Block a user