1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 05:53:42 +00:00

Merge branch 'main' into km/pm-14445-crypto

This commit is contained in:
Maciej Zieniuk
2025-02-28 00:00:04 +00:00
183 changed files with 8751 additions and 3921 deletions

View File

@@ -22,6 +22,18 @@
description: "Determined by Angular",
enabled: false,
},
{
matchSourceUrls: [
"https://github.com/angular-eslint/angular-eslint",
"https://github.com/angular/angular-cli",
"https://github.com/angular/angular",
"https://github.com/angular/components",
"https://github.com/ng-select/ng-select",
],
matchUpdateTypes: ["major"],
description: "Manually updated using ng update",
enabled: false,
},
{
matchPackageNames: ["typescript", "zone.js"],
matchUpdateTypes: "patch",
@@ -90,12 +102,8 @@
},
{
matchPackageNames: [
"@angular-eslint/eslint-plugin-template",
"@angular-eslint/eslint-plugin",
"@angular-eslint/schematics",
"@angular-eslint/template-parser",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
"angular-eslint",
"eslint-config-prettier",
"eslint-import-resolver-typescript",
"eslint-plugin-import",
@@ -106,6 +114,7 @@
"eslint",
"husky",
"lint-staged",
"typescript-eslint",
],
groupName: "Linting minor-patch",
matchUpdateTypes: ["minor", "patch"],
@@ -114,6 +123,7 @@
matchPackageNames: [
"@emotion/css",
"@webcomponents/custom-elements",
"bytes",
"concurrently",
"cross-env",
"del",

View File

@@ -205,6 +205,9 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install AST
run: dotnet tool install --global AzureSignTool --version 4.0.1
- name: Setup Windows builder
run: |
choco install checksum --no-progress
@@ -273,6 +276,24 @@ jobs:
ResourceHacker -open version-info.rc -save version-info.res -action compile
ResourceHacker -open %WIN_PKG_BUILT% -save %WIN_PKG_BUILT% -action addoverwrite -resource version-info.res
- name: Login to Azure
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "code-signing-vault-url,
code-signing-client-id,
code-signing-tenant-id,
code-signing-client-secret,
code-signing-cert-name"
- name: Install
run: npm ci
working-directory: ./
@@ -300,6 +321,18 @@ jobs:
- name: Build & Package Windows
run: npm run dist:${{ matrix.license_type.build_prefix }}:win --quiet
- name: Sign executable
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
shell: pwsh
env:
SIGNING_VAULT_URL: ${{ steps.retrieve-secrets.outputs.code-signing-vault-url }}
SIGNING_CLIENT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-client-id }}
SIGNING_TENANT_ID: ${{ steps.retrieve-secrets.outputs.code-signing-tenant-id }}
SIGNING_CLIENT_SECRET: ${{ steps.retrieve-secrets.outputs.code-signing-client-secret }}
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
EXE_PATH: dist/${{ matrix.license_type.build_prefix }}/windows/bw.exe
run: . .\scripts\sign-cli.ps1
- name: Package Chocolatey
shell: pwsh
if: ${{ matrix.license_type.build_prefix == 'bit' }}

View File

@@ -392,8 +392,7 @@ jobs:
run: node build.js cross-platform
- name: Build
run: |
npm run build
run: npm run build
- name: Pack
if: ${{ needs.setup.outputs.has_secrets == 'false' }}

View File

@@ -1397,14 +1397,14 @@
},
"useAnotherTwoStepMethod": {
"message": "Use another two-step login method"
},
},
"selectAnotherMethod": {
"message": "Select another method",
"description": "Select another two-step login method"
},
"useYourRecoveryCode": {
"message": "Use your recovery code"
},
},
"insertYubiKey": {
"message": "Insert your YubiKey into your computer's USB port, then touch its button."
},
@@ -2143,6 +2143,15 @@
"vaultTimeoutAction1": {
"message": "Timeout action"
},
"newCustomizationOptionsCalloutTitle": {
"message": "New customization options"
},
"newCustomizationOptionsCalloutContent": {
"message": "Customize your vault experience with quick copy actions, compact mode, and more!"
},
"newCustomizationOptionsCalloutLink": {
"message": "View all Appearance settings"
},
"lock": {
"message": "Lock",
"description": "Verb form: to make secure or inaccessible by"
@@ -2437,8 +2446,17 @@
"atRiskPasswords": {
"message": "At-risk passwords"
},
"atRiskPasswordsDescSingleOrg": {
"message": "$ORGANIZATION$ is requesting you change the $COUNT$ passwords because they are at risk.",
"atRiskPasswordDescSingleOrg": {
"message": "$ORGANIZATION$ is requesting you change one password because it is at-risk.",
"placeholders": {
"organization": {
"content": "$1",
"example": "Acme Corp"
}
}
},
"atRiskPasswordsDescSingleOrgPlural": {
"message": "$ORGANIZATION$ is requesting you change the $COUNT$ passwords because they are at-risk.",
"placeholders": {
"organization": {
"content": "$1",
@@ -2450,8 +2468,8 @@
}
}
},
"atRiskPasswordsDescMultiOrg": {
"message": "Your organizations are requesting you change the $COUNT$ passwords because they are at risk.",
"atRiskPasswordsDescMultiOrgPlural": {
"message": "Your organizations are requesting you change the $COUNT$ passwords because they are at-risk.",
"placeholders": {
"count": {
"content": "$1",
@@ -2477,6 +2495,36 @@
"changeAtRiskPasswordsFasterDesc": {
"message": "Update your settings so you can quickly autofill your passwords and generate new ones"
},
"reviewAtRiskLogins": {
"message": "Review at-risk logins"
},
"reviewAtRiskPasswords": {
"message": "Review at-risk passwords"
},
"reviewAtRiskLoginsSlideDesc": {
"message": "Your organization passwords are at-risk because they are weak, reused, and/or exposed.",
"description": "Description of the review at-risk login slide on the at-risk password page carousel"
},
"reviewAtRiskLoginSlideImgAlt": {
"message": "Illustration of a list of logins that are at-risk"
},
"generatePasswordSlideDesc": {
"message": "Quickly generate a strong, unique password with the Bitwarden autofill menu on the at-risk site.",
"description": "Description of the generate password slide on the at-risk password page carousel"
},
"generatePasswordSlideImgAlt": {
"message": "Illustration of the Bitwarden autofill menu displaying a generated password"
},
"updateInBitwarden": {
"message": "Update in Bitwarden"
},
"updateInBitwardenSlideDesc": {
"message": "Bitwarden will then prompt you to update the password in the password manager.",
"description": "Description of the update in Bitwarden slide on the at-risk password page carousel"
},
"updateInBitwardenSlideImgAlt": {
"message": "Illustration of a Bitwardens notification prompting the user to update the login"
},
"turnOnAutofill": {
"message": "Turn on autofill"
},
@@ -4227,6 +4275,20 @@
}
}
},
"copyFieldValue": {
"message": "Copy $FIELD$, $VALUE$",
"description": "Title for a button that copies a field value to the clipboard.",
"placeholders": {
"field": {
"content": "$1",
"example": "Username"
},
"value": {
"content": "$2",
"example": "Foo"
}
}
},
"noValuesToCopy": {
"message": "No values to copy"
},

View File

@@ -9,7 +9,10 @@ import {
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import {
Environment,
EnvironmentService,
} from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { UserId } from "@bitwarden/common/types/guid";
@@ -21,6 +24,12 @@ describe("AccountSwitcherService", () => {
let activeAccountSubject: BehaviorSubject<Account | null>;
let authStatusSubject: ReplaySubject<Record<UserId, AuthenticationStatus>>;
let envBSubject: BehaviorSubject<Environment | undefined>;
const mockHostName = "mockHostName";
const mockEnv: Partial<Environment> = {
getHostname: () => mockHostName,
};
const accountService = mock<AccountService>();
const avatarService = mock<AvatarService>();
const messagingService = mock<MessagingService>();
@@ -41,6 +50,9 @@ describe("AccountSwitcherService", () => {
accountService.activeAccount$ = activeAccountSubject;
authService.authStatuses$ = authStatusSubject;
envBSubject = new BehaviorSubject<Environment | undefined>(mockEnv as Environment);
environmentService.getEnvironment$.mockReturnValue(envBSubject);
accountSwitcherService = new AccountSwitcherService(
accountService,
avatarService,
@@ -79,11 +91,16 @@ describe("AccountSwitcherService", () => {
expect(accounts).toHaveLength(3);
expect(accounts[0].id).toBe("1");
expect(accounts[0].isActive).toBeTruthy();
expect(accounts[0].server).toBe(mockHostName);
expect(accounts[1].id).toBe("2");
expect(accounts[1].isActive).toBeFalsy();
expect(accounts[1].server).toBe(mockHostName);
expect(accounts[2].id).toBe("addAccount");
expect(accounts[2].isActive).toBeFalsy();
expect(accounts[2].server).toBe(undefined);
});
it.each([5, 6])(

View File

@@ -66,11 +66,12 @@ export class AccountSwitcherService {
const hasMaxAccounts = loggedInIds.length >= this.ACCOUNT_LIMIT;
const options: AvailableAccount[] = await Promise.all(
loggedInIds.map(async (id: UserId) => {
const userEnv = await firstValueFrom(this.environmentService.getEnvironment$(id));
return {
name: accounts[id].name ?? accounts[id].email,
email: accounts[id].email,
id: id,
server: (await this.environmentService.getEnvironment(id))?.getHostname(),
server: userEnv?.getHostname(),
status: accountStatuses[id],
isActive: id === activeAccount?.id,
avatarColor: await firstValueFrom(

View File

@@ -0,0 +1,98 @@
import { css } from "@emotion/css";
import { html } from "lit";
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import {
NotificationBarIframeInitData,
NotificationTypes,
NotificationType,
} from "../../../notification/abstractions/notification-bar";
import { themes, spacing } from "../constants/styles";
import { NotificationConfirmationBody } from "./confirmation";
import {
NotificationHeader,
componentClassPrefix as notificationHeaderClassPrefix,
} from "./header";
export function NotificationConfirmationContainer({
error,
handleCloseNotification,
i18n,
theme = ThemeTypes.Light,
type,
}: NotificationBarIframeInitData & {
handleCloseNotification: (e: Event) => void;
} & {
error: string;
i18n: { [key: string]: string };
type: NotificationType;
}) {
const headerMessage = getHeaderMessage(i18n, type, error);
const confirmationMessage = getConfirmationMessage(i18n, type, error);
const buttonText = error ? i18n.newItem : i18n.view;
return html`
<div class=${notificationContainerStyles(theme)}>
${NotificationHeader({
handleCloseNotification,
message: headerMessage,
theme,
})}
${NotificationConfirmationBody({
error: error,
buttonText,
confirmationMessage,
theme,
})}
</div>
`;
}
const notificationContainerStyles = (theme: Theme) => css`
position: absolute;
right: 20px;
border: 1px solid ${themes[theme].secondary["300"]};
border-radius: ${spacing["4"]};
box-shadow: -2px 4px 6px 0px #0000001a;
background-color: ${themes[theme].background.alt};
width: 400px;
overflow: hidden;
[class*="${notificationHeaderClassPrefix}-"] {
border-radius: ${spacing["4"]} ${spacing["4"]} 0 0;
border-bottom: 0.5px solid ${themes[theme].secondary["300"]};
}
`;
function getConfirmationMessage(
i18n: { [key: string]: string },
type?: NotificationType,
error?: string,
) {
if (error) {
return i18n.saveFailureDetails;
}
return type === "add" ? i18n.loginSaveSuccessDetails : i18n.loginUpdateSuccessDetails;
}
function getHeaderMessage(
i18n: { [key: string]: string },
type?: NotificationType,
error?: string,
) {
if (error) {
return i18n.saveFailure;
}
switch (type) {
case NotificationTypes.Add:
return i18n.loginSaveSuccess;
case NotificationTypes.Change:
return i18n.loginUpdateSuccess;
case NotificationTypes.Unlock:
return "";
default:
return undefined;
}
}

View File

@@ -17,12 +17,12 @@ const { css } = createEmotion({
export function NotificationHeader({
message,
standalone,
standalone = false,
theme = ThemeTypes.Light,
handleCloseNotification,
}: {
message?: string;
standalone: boolean;
standalone?: boolean;
theme: Theme;
handleCloseNotification: (e: Event) => void;
}) {
@@ -49,7 +49,7 @@ const notificationHeaderStyles = ({
display: flex;
align-items: center;
justify-content: flex-start;
background-color: ${themes[theme].background.alt};
background-color: ${themes[theme].background};
padding: 12px 16px 8px 16px;
white-space: nowrap;

View File

@@ -7,6 +7,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l
import type { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { AdjustNotificationBarMessageData } from "../background/abstractions/notification.background";
import { NotificationConfirmationContainer } from "../content/components/notification/confirmation-container";
import { NotificationContainer } from "../content/components/notification/container";
import { buildSvgDomElement } from "../utils";
import { circleCheckIcon } from "../utils/svg-icons";
@@ -22,12 +23,17 @@ const logService = new ConsoleLogService(false);
let notificationBarIframeInitData: NotificationBarIframeInitData = {};
let windowMessageOrigin: string;
let useComponentBar = false;
const notificationBarWindowMessageHandlers: NotificationBarWindowMessageHandlers = {
initNotificationBar: ({ message }) => initNotificationBar(message),
saveCipherAttemptCompleted: ({ message }) => handleSaveCipherAttemptCompletedMessage(message),
saveCipherAttemptCompleted: ({ message }) =>
useComponentBar
? handleSaveCipherConfirmation(message)
: handleSaveCipherAttemptCompletedMessage(message),
};
globalThis.addEventListener("load", load);
function load() {
setupWindowMessageListener();
sendPlatformMessage({ command: "notificationRefreshFlagValue" }, (flagValue) => {
@@ -35,7 +41,6 @@ function load() {
applyNotificationBarStyle();
});
}
function applyNotificationBarStyle() {
if (!useComponentBar) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -44,16 +49,8 @@ function applyNotificationBarStyle() {
postMessageToParent({ command: "initNotificationBar" });
}
function initNotificationBar(message: NotificationBarWindowMessage) {
const { initData } = message;
if (!initData) {
return;
}
notificationBarIframeInitData = initData;
const { isVaultLocked, theme } = notificationBarIframeInitData;
const i18n = {
function getI18n() {
return {
appName: chrome.i18n.getMessage("appName"),
close: chrome.i18n.getMessage("close"),
never: chrome.i18n.getMessage("never"),
@@ -74,20 +71,30 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
updateLoginPrompt: "Update existing login?",
loginSaveSuccess: "Login saved",
loginSaveSuccessDetails: "Login saved to Bitwarden.",
loginUpdateSuccess: "Login saved",
loginUpdateSuccess: "Login updated",
loginUpdateSuccessDetails: "Login updated in Bitwarden.",
saveFailure: "Error saving",
saveFailureDetails: "Oh no! We couldn't save this. Try entering the details as a New item",
newItem: "New item",
view: "View",
};
}
function initNotificationBar(message: NotificationBarWindowMessage) {
const { initData } = message;
if (!initData) {
return;
}
notificationBarIframeInitData = initData;
const { isVaultLocked, theme } = notificationBarIframeInitData;
const i18n = getI18n();
const resolvedTheme = getResolvedTheme(theme);
if (useComponentBar) {
document.body.innerHTML = "";
// Current implementations utilize a require for scss files which creates the need to remove the node.
document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove());
const themeType = getTheme(globalThis, theme);
// There are other possible passed theme values, but for now, resolve to dark or light
const resolvedTheme: Theme = themeType === ThemeTypes.Dark ? ThemeTypes.Dark : ThemeTypes.Light;
sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, (cipherData) => {
// @TODO use context to avoid prop drilling
@@ -105,77 +112,71 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
document.body,
);
});
}
} else {
setNotificationBarTheme();
setNotificationBarTheme();
(document.getElementById("logo") as HTMLImageElement).src = isVaultLocked
? chrome.runtime.getURL("images/icon38_locked.png")
: chrome.runtime.getURL("images/icon38.png");
(document.getElementById("logo") as HTMLImageElement).src = isVaultLocked
? chrome.runtime.getURL("images/icon38_locked.png")
: chrome.runtime.getURL("images/icon38.png");
setupLogoLink(i18n);
setupLogoLink(i18n);
// i18n for "Add" template
const addTemplate = document.getElementById("template-add") as HTMLTemplateElement;
// i18n for "Add" template
const addTemplate = document.getElementById("template-add") as HTMLTemplateElement;
const neverButton = addTemplate.content.getElementById("never-save");
neverButton.textContent = i18n.never;
const neverButton = addTemplate.content.getElementById("never-save");
neverButton.textContent = i18n.never;
const selectFolder = addTemplate.content.getElementById("select-folder");
selectFolder.hidden = isVaultLocked || removeIndividualVault();
selectFolder.setAttribute("aria-label", i18n.folder);
const selectFolder = addTemplate.content.getElementById("select-folder");
selectFolder.hidden = isVaultLocked || removeIndividualVault();
selectFolder.setAttribute("aria-label", i18n.folder);
const addButton = addTemplate.content.getElementById("add-save");
addButton.textContent = i18n.notificationAddSave;
const addButton = addTemplate.content.getElementById("add-save");
addButton.textContent = i18n.notificationAddSave;
const addEditButton = addTemplate.content.getElementById("add-edit");
// If Remove Individual Vault policy applies, "Add" opens the edit tab, so we hide the Edit button
addEditButton.hidden = removeIndividualVault();
addEditButton.textContent = i18n.notificationEdit;
const addEditButton = addTemplate.content.getElementById("add-edit");
// If Remove Individual Vault policy applies, "Add" opens the edit tab, so we hide the Edit button
addEditButton.hidden = removeIndividualVault();
addEditButton.textContent = i18n.notificationEdit;
addTemplate.content.getElementById("add-text").textContent = i18n.notificationAddDesc;
addTemplate.content.getElementById("add-text").textContent = i18n.notificationAddDesc;
// i18n for "Change" (update password) template
const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
// i18n for "Change" (update password) template
const changeTemplate = document.getElementById("template-change") as HTMLTemplateElement;
const changeButton = changeTemplate.content.getElementById("change-save");
changeButton.textContent = i18n.notificationChangeSave;
const changeButton = changeTemplate.content.getElementById("change-save");
changeButton.textContent = i18n.notificationChangeSave;
const changeEditButton = changeTemplate.content.getElementById("change-edit");
changeEditButton.textContent = i18n.notificationEdit;
const changeEditButton = changeTemplate.content.getElementById("change-edit");
changeEditButton.textContent = i18n.notificationEdit;
changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
changeTemplate.content.getElementById("change-text").textContent = i18n.notificationChangeDesc;
// i18n for "Unlock" (unlock extension) template
const unlockTemplate = document.getElementById("template-unlock") as HTMLTemplateElement;
// i18n for "Unlock" (unlock extension) template
const unlockTemplate = document.getElementById("template-unlock") as HTMLTemplateElement;
const unlockButton = unlockTemplate.content.getElementById("unlock-vault");
unlockButton.textContent = i18n.notificationUnlock;
const unlockButton = unlockTemplate.content.getElementById("unlock-vault");
unlockButton.textContent = i18n.notificationUnlock;
unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc;
unlockTemplate.content.getElementById("unlock-text").textContent = i18n.notificationUnlockDesc;
// i18n for body content
const closeButton = document.getElementById("close-button");
closeButton.title = i18n.close;
// i18n for body content
const closeButton = document.getElementById("close-button");
closeButton.title = i18n.close;
const notificationType = initData.type;
if (notificationType === "add") {
handleTypeAdd();
} else if (notificationType === "change") {
handleTypeChange();
} else if (notificationType === "unlock") {
handleTypeUnlock();
}
const notificationType = initData.type;
if (notificationType === "add") {
handleTypeAdd();
} else if (notificationType === "change") {
handleTypeChange();
} else if (notificationType === "unlock") {
handleTypeUnlock();
}
closeButton.addEventListener("click", handleCloseNotification);
closeButton.addEventListener("click", handleCloseNotification);
globalThis.addEventListener("resize", adjustHeight);
adjustHeight();
function handleCloseNotification(e: Event) {
e.preventDefault();
sendPlatformMessage({
command: "bgCloseNotificationBar",
});
globalThis.addEventListener("resize", adjustHeight);
adjustHeight();
}
function handleEditOrUpdateAction(e: Event) {
const notificationType = initData.type;
@@ -183,6 +184,12 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
notificationType === "add" ? sendSaveCipherMessage(true) : sendSaveCipherMessage(false);
}
}
function handleCloseNotification(e: Event) {
e.preventDefault();
sendPlatformMessage({
command: "bgCloseNotificationBar",
});
}
function handleSaveAction(e: Event) {
e.preventDefault();
@@ -282,6 +289,27 @@ function handleSaveCipherAttemptCompletedMessage(message: NotificationBarWindowM
);
}
function handleSaveCipherConfirmation(message: NotificationBarWindowMessage) {
const { theme, type } = notificationBarIframeInitData;
const { error } = message;
const i18n = getI18n();
const resolvedTheme = getResolvedTheme(theme);
globalThis.setTimeout(() => sendPlatformMessage({ command: "bgCloseNotificationBar" }), 5000);
return render(
NotificationConfirmationContainer({
...notificationBarIframeInitData,
type: type as NotificationType,
theme: resolvedTheme,
handleCloseNotification,
i18n,
error,
}),
document.body,
);
}
function handleTypeUnlock() {
setContent(document.getElementById("template-unlock") as HTMLTemplateElement);
@@ -395,6 +423,14 @@ function getTheme(globalThis: any, theme: NotificationBarIframeInitData["theme"]
return theme;
}
function getResolvedTheme(theme: Theme) {
const themeType = getTheme(globalThis, theme);
// There are other possible passed theme values, but for now, resolve to dark or light
const resolvedTheme: Theme = themeType === ThemeTypes.Dark ? ThemeTypes.Dark : ThemeTypes.Light;
return resolvedTheme;
}
function setNotificationBarTheme() {
const theme = getTheme(globalThis, notificationBarIframeInitData.theme);

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import "core-js/proposals/explicit-resource-management";
import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs";
import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common";
@@ -1290,7 +1292,7 @@ export default class MainBackground {
}
this.containerService.attachToGlobal(self);
await this.sdkLoadService.load();
await this.sdkLoadService.loadAndInit();
// Only the "true" background should run migrations
await this.stateService.init({ runMigrations: true });

View File

@@ -45,14 +45,14 @@
#refreshBtn
type="button"
(click)="refresh()"
[disabled]="$any(refreshBtn).loading"
[disabled]="$any(refreshBtn).loading()"
[appApiAction]="refreshPromise"
bitButton
>
<span [hidden]="$any(refreshBtn).loading">{{ "premiumRefresh" | i18n }}</span>
<span [hidden]="$any(refreshBtn).loading()">{{ "premiumRefresh" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(refreshBtn).loading"
[hidden]="!$any(refreshBtn).loading()"
aria-hidden="true"
></i>
</button>

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -7,7 +7,7 @@
<ul class="tw-flex tw-flex-1 tw-mb-0 tw-p-0">
<li *ngFor="let button of navButtons" class="tw-flex-1 tw-list-none">
<button
class="tw-w-full tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 bit-compact:tw-pb-1 tw-pt-3 bit-compact:tw-pt-2 tw-bg-transparent tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-600"
class="tw-w-full tw-flex tw-flex-col tw-items-center tw-gap-1 tw-px-0.5 tw-pb-2 bit-compact:tw-pb-1 tw-pt-3 bit-compact:tw-pt-2 tw-text-sm tw-bg-transparent tw-no-underline hover:tw-no-underline hover:tw-text-primary-600 hover:tw-bg-primary-100 tw-border-2 tw-border-solid tw-border-transparent focus-visible:tw-rounded-lg focus-visible:tw-border-primary-600"
[ngClass]="rla.isActive ? 'tw-font-bold tw-text-primary-600' : 'tw-text-muted'"
title="{{ button.label | i18n }}"
[routerLink]="button.page"

View File

@@ -60,8 +60,10 @@ async function importModule(): Promise<GlobalWithWasmInit["initSdk"]> {
return (globalThis as GlobalWithWasmInit).initSdk;
}
export class BrowserSdkLoadService implements SdkLoadService {
constructor(readonly logService: LogService) {}
export class BrowserSdkLoadService extends SdkLoadService {
constructor(readonly logService: LogService) {
super();
}
async load(): Promise<void> {
const startTime = performance.now();

View File

@@ -17,7 +17,6 @@ import {
tdeDecryptionRequiredGuard,
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
AnonLayoutWrapperComponent,
@@ -45,7 +44,6 @@ import {
UserLockIcon,
VaultIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LockComponent } from "@bitwarden/key-management-ui";
import {
NewDeviceVerificationNoticePageOneComponent,
@@ -245,11 +243,7 @@ const routes: Routes = [
{
path: "device-verification",
component: ExtensionAnonLayoutWrapperComponent,
canActivate: [
canAccessFeature(FeatureFlag.NewDeviceVerification),
unauthGuardFn(),
activeAuthGuard(),
],
canActivate: [unauthGuardFn(), activeAuthGuard()],
children: [{ path: "", component: NewDeviceVerificationComponent }],
data: {
pageIcon: DeviceVerificationIcon,

View File

@@ -1,2 +1,3 @@
import "core-js/stable";
import "core-js/proposals/explicit-resource-management";
import "zone.js";

View File

@@ -32,7 +32,7 @@ export class InitService {
init() {
return async () => {
await this.sdkLoadService.load();
await this.sdkLoadService.loadAndInit();
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
await this.i18nService.init();
this.twoFactorService.init();

View File

@@ -0,0 +1,54 @@
<bit-simple-dialog hideIcon>
<div bitDialogContent>
<vault-carousel label="Placeholder" (slideChange)="onSlideChange($event)">
<vault-carousel-slide [label]="'reviewAtRiskLogins' | i18n">
<img
class="tw-max-w-full tw-max-h-40"
src="../../../../images/at-risk-password-carousel/review_at-risk_logins.light.png"
appDarkImgSrc="../../../../images/at-risk-password-carousel/review_at-risk_logins.dark.png"
[alt]="'reviewAtRiskLoginSlideImgAlt' | i18n"
/>
<h2 bitTypography="h2" class="tw-mt-8">{{ "reviewAtRiskLogins" | i18n }}</h2>
<p bitTypography="body1">
{{ "reviewAtRiskLoginsSlideDesc" | i18n }}
</p>
</vault-carousel-slide>
<vault-carousel-slide [label]="'generatePassword' | i18n">
<img
class="tw-max-w-full tw-max-h-40"
src="../../../../images/at-risk-password-carousel/generate_password.light.png"
appDarkImgSrc="../../../../images/at-risk-password-carousel/generate_password.dark.png"
[alt]="'generatePasswordSlideImgAlt' | i18n"
/>
<h2 bitTypography="h2" class="tw-mt-8">{{ "generatePassword" | i18n }}</h2>
<p bitTypography="body1">
{{ "generatePasswordSlideDesc" | i18n }}
</p>
</vault-carousel-slide>
<vault-carousel-slide [label]="'updateInBitwarden' | i18n">
<img
class="tw-max-w-full tw-max-h-40"
src="../../../../images/at-risk-password-carousel/update_login.light.png"
appDarkImgSrc="../../../../images/at-risk-password-carousel/update_login.dark.png"
[alt]="'updateInBitwardenSlideImgAlt' | i18n"
/>
<h2 bitTypography="h2" class="tw-mt-8">{{ "updateInBitwarden" | i18n }}</h2>
<p bitTypography="body1">
{{ "updateInBitwardenSlideDesc" | i18n }}
</p>
</vault-carousel-slide>
</vault-carousel>
</div>
<div bitDialogFooter class="tw-w-full">
<button
type="button"
bitButton
buttonType="primary"
block
[disabled]="!dismissBtnEnabled()"
(click)="dismiss()"
>
{{ "reviewAtRiskPasswords" | i18n }}
</button>
</div>
</bit-simple-dialog>

View File

@@ -0,0 +1,46 @@
import { DialogRef } from "@angular/cdk/dialog";
import { Component, inject, signal } from "@angular/core";
import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { DarkImageSourceDirective, VaultCarouselModule } from "@bitwarden/vault";
export enum AtRiskCarouselDialogResult {
Dismissed = "dismissed",
}
@Component({
selector: "vault-at-risk-carousel-dialog",
templateUrl: "./at-risk-carousel-dialog.component.html",
imports: [
DialogModule,
VaultCarouselModule,
TypographyModule,
ButtonModule,
DarkImageSourceDirective,
I18nPipe,
],
standalone: true,
})
export class AtRiskCarouselDialogComponent {
private dialogRef = inject(DialogRef);
protected dismissBtnEnabled = signal(false);
protected async dismiss() {
this.dialogRef.close(AtRiskCarouselDialogResult.Dismissed);
}
protected onSlideChange(slideIndex: number) {
// Only enable the dismiss button on the last slide
if (slideIndex === 2) {
this.dismissBtnEnabled.set(true);
}
}
static open(dialogService: DialogService) {
return dialogService.open<AtRiskCarouselDialogResult>(AtRiskCarouselDialogComponent, {
disableClose: true,
});
}
}

View File

@@ -2,18 +2,27 @@ import { inject, Injectable } from "@angular/core";
import { map, Observable } from "rxjs";
import {
BANNERS_DISMISSED_DISK,
AT_RISK_PASSWORDS_PAGE_DISK,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
export const AT_RISK_PASSWORD_AUTOFILL_CALLOUT_DISMISSED_KEY = new UserKeyDefinition<boolean>(
BANNERS_DISMISSED_DISK,
"atRiskPasswordAutofillBannerDismissed",
const AUTOFILL_CALLOUT_DISMISSED_KEY = new UserKeyDefinition<boolean>(
AT_RISK_PASSWORDS_PAGE_DISK,
"autofillCalloutDismissed",
{
deserializer: (bannersDismissed) => bannersDismissed,
clearOn: [], // Do not clear dismissed banners
clearOn: [], // Do not clear dismissed callout
},
);
const GETTING_STARTED_CAROUSEL_DISMISSED_KEY = new UserKeyDefinition<boolean>(
AT_RISK_PASSWORDS_PAGE_DISK,
"gettingStartedCarouselDismissed",
{
deserializer: (bannersDismissed) => bannersDismissed,
clearOn: [], // Do not clear dismissed carousel
},
);
@@ -23,13 +32,23 @@ export class AtRiskPasswordPageService {
isCalloutDismissed(userId: UserId): Observable<boolean> {
return this.stateProvider
.getUser(userId, AT_RISK_PASSWORD_AUTOFILL_CALLOUT_DISMISSED_KEY)
.getUser(userId, AUTOFILL_CALLOUT_DISMISSED_KEY)
.state$.pipe(map((dismissed) => !!dismissed));
}
async dismissCallout(userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, AUTOFILL_CALLOUT_DISMISSED_KEY).update(() => true);
}
isGettingStartedDismissed(userId: UserId): Observable<boolean> {
return this.stateProvider
.getUser(userId, GETTING_STARTED_CAROUSEL_DISMISSED_KEY)
.state$.pipe(map((dismissed) => !!dismissed));
}
async dismissGettingStarted(userId: UserId): Promise<void> {
await this.stateProvider
.getUser(userId, AT_RISK_PASSWORD_AUTOFILL_CALLOUT_DISMISSED_KEY)
.getUser(userId, GETTING_STARTED_CAROUSEL_DISMISSED_KEY)
.update(() => true);
}
}

View File

@@ -6,7 +6,7 @@
</popup-header>
<bit-callout
*ngIf="!(inlineAutofillSettingEnabled$ | async) && !(calloutDismissed$ | async)"
*ngIf="showAutofillCallout$ | async"
type="info"
[title]="'changeAtRiskPasswordsFaster' | i18n"
data-testid="autofill-callout"
@@ -18,6 +18,7 @@
buttonType="primary"
(click)="activateInlineAutofillMenuVisibility()"
data-testid="turn-on-autofill-button"
class="tw-mr-2"
>
{{ "turnOnAutofill" | i18n }}
</button>

View File

@@ -16,7 +16,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ToastService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import {
ChangeLoginPasswordService,
DefaultChangeLoginPasswordService,
@@ -28,6 +28,7 @@ import {
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
import { AtRiskCarouselDialogResult } from "../at-risk-carousel-dialog/at-risk-carousel-dialog.component";
import { AtRiskPasswordPageService } from "./at-risk-password-page.service";
import { AtRiskPasswordsComponent } from "./at-risk-passwords.component";
@@ -73,6 +74,7 @@ describe("AtRiskPasswordsComponent", () => {
const mockToastService = mock<ToastService>();
const mockAtRiskPasswordPageService = mock<AtRiskPasswordPageService>();
const mockChangeLoginPasswordService = mock<ChangeLoginPasswordService>();
const mockDialogService = mock<DialogService>();
beforeEach(async () => {
mockTasks$ = new BehaviorSubject<SecurityTask[]>([
@@ -109,6 +111,7 @@ describe("AtRiskPasswordsComponent", () => {
calloutDismissed$ = new BehaviorSubject<boolean>(false);
setInlineMenuVisibility.mockClear();
mockToastService.showToast.mockClear();
mockDialogService.open.mockClear();
mockAtRiskPasswordPageService.isCalloutDismissed.mockReturnValue(calloutDismissed$);
await TestBed.configureTestingModule({
@@ -162,6 +165,7 @@ describe("AtRiskPasswordsComponent", () => {
providers: [
AtRiskPasswordPageService,
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
DialogService,
],
},
add: {
@@ -169,6 +173,7 @@ describe("AtRiskPasswordsComponent", () => {
providers: [
{ provide: AtRiskPasswordPageService, useValue: mockAtRiskPasswordPageService },
{ provide: ChangeLoginPasswordService, useValue: mockChangeLoginPasswordService },
{ provide: DialogService, useValue: mockDialogService },
],
},
})
@@ -193,8 +198,27 @@ describe("AtRiskPasswordsComponent", () => {
describe("pageDescription$", () => {
it("should use single org description when tasks belong to one org", async () => {
const description = await firstValueFrom(component["pageDescription$"]);
expect(description).toBe("atRiskPasswordsDescSingleOrg");
// Single task
let description = await firstValueFrom(component["pageDescription$"]);
expect(description).toBe("atRiskPasswordDescSingleOrg");
// Multiple tasks
mockTasks$.next([
{
id: "task",
organizationId: "org",
cipherId: "cipher",
type: SecurityTaskType.UpdateAtRiskCredential,
} as SecurityTask,
{
id: "task2",
organizationId: "org",
cipherId: "cipher2",
type: SecurityTaskType.UpdateAtRiskCredential,
} as SecurityTask,
]);
description = await firstValueFrom(component["pageDescription$"]);
expect(description).toBe("atRiskPasswordsDescSingleOrgPlural");
});
it("should use multiple org description when tasks belong to multiple orgs", async () => {
@@ -213,7 +237,7 @@ describe("AtRiskPasswordsComponent", () => {
} as SecurityTask,
]);
const description = await firstValueFrom(component["pageDescription$"]);
expect(description).toBe("atRiskPasswordsDescMultiOrg");
expect(description).toBe("atRiskPasswordsDescMultiOrgPlural");
});
});
@@ -269,4 +293,31 @@ describe("AtRiskPasswordsComponent", () => {
});
});
});
describe("getting started carousel", () => {
it("should open the carousel automatically if the user has not dismissed it", async () => {
mockAtRiskPasswordPageService.isGettingStartedDismissed.mockReturnValue(of(false));
mockDialogService.open.mockReturnValue({ closed: of(undefined) } as any);
await component.ngOnInit();
expect(mockDialogService.open).toHaveBeenCalled();
});
it("should not open the carousel automatically if the user has already dismissed it", async () => {
mockDialogService.open.mockClear(); // Need to clear the mock since the component is already initialized once
mockAtRiskPasswordPageService.isGettingStartedDismissed.mockReturnValue(of(true));
mockDialogService.open.mockReturnValue({ closed: of(undefined) } as any);
await component.ngOnInit();
expect(mockDialogService.open).not.toHaveBeenCalled();
});
it("should mark the carousel as dismissed when the user dismisses it", async () => {
mockAtRiskPasswordPageService.isGettingStartedDismissed.mockReturnValue(of(false));
mockDialogService.open.mockReturnValue({
closed: of(AtRiskCarouselDialogResult.Dismissed),
} as any);
await component.ngOnInit();
expect(mockDialogService.open).toHaveBeenCalled();
expect(mockAtRiskPasswordPageService.dismissGettingStarted).toHaveBeenCalled();
});
});
});

View File

@@ -1,5 +1,5 @@
import { CommonModule } from "@angular/common";
import { Component, inject, signal } from "@angular/core";
import { Component, inject, OnInit, signal } from "@angular/core";
import { Router } from "@angular/router";
import { combineLatest, firstValueFrom, map, of, shareReplay, startWith, switchMap } from "rxjs";
@@ -19,6 +19,8 @@ import {
BadgeModule,
ButtonModule,
CalloutModule,
DialogModule,
DialogService,
ItemModule,
ToastService,
TypographyModule,
@@ -30,11 +32,16 @@ import {
PasswordRepromptService,
SecurityTaskType,
TaskService,
VaultCarouselModule,
} from "@bitwarden/vault";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
import {
AtRiskCarouselDialogComponent,
AtRiskCarouselDialogResult,
} from "../at-risk-carousel-dialog/at-risk-carousel-dialog.component";
import { AtRiskPasswordPageService } from "./at-risk-password-page.service";
@@ -50,6 +57,8 @@ import { AtRiskPasswordPageService } from "./at-risk-password-page.service";
CalloutModule,
ButtonModule,
BadgeModule,
DialogModule,
VaultCarouselModule,
],
providers: [
AtRiskPasswordPageService,
@@ -59,7 +68,7 @@ import { AtRiskPasswordPageService } from "./at-risk-password-page.service";
standalone: true,
templateUrl: "./at-risk-passwords.component.html",
})
export class AtRiskPasswordsComponent {
export class AtRiskPasswordsComponent implements OnInit {
private taskService = inject(TaskService);
private organizationService = inject(OrganizationService);
private cipherService = inject(CipherService);
@@ -72,6 +81,7 @@ export class AtRiskPasswordsComponent {
private atRiskPasswordPageService = inject(AtRiskPasswordPageService);
private changeLoginPasswordService = inject(ChangeLoginPasswordService);
private platformUtilsService = inject(PlatformUtilsService);
private dialogService = inject(DialogService);
/**
* The cipher that is currently being launched. Used to show a loading spinner on the badge button.
@@ -105,14 +115,23 @@ export class AtRiskPasswordsComponent {
startWith(true),
);
protected calloutDismissed$ = this.activeUserData$.pipe(
private calloutDismissed$ = this.activeUserData$.pipe(
switchMap(({ userId }) => this.atRiskPasswordPageService.isCalloutDismissed(userId)),
);
protected inlineAutofillSettingEnabled$ = this.autofillSettingsService.inlineMenuVisibility$.pipe(
private inlineAutofillSettingEnabled$ = this.autofillSettingsService.inlineMenuVisibility$.pipe(
map((setting) => setting !== AutofillOverlayVisibility.Off),
);
protected showAutofillCallout$ = combineLatest([
this.calloutDismissed$,
this.inlineAutofillSettingEnabled$,
]).pipe(
map(([calloutDismissed, inlineAutofillSettingEnabled]) => {
return !calloutDismissed && !inlineAutofillSettingEnabled;
}),
startWith(false),
);
protected atRiskItems$ = this.activeUserData$.pipe(
map(({ tasks, ciphers }) =>
tasks
@@ -133,14 +152,37 @@ export class AtRiskPasswordsComponent {
const [orgId] = orgIds;
return this.organizationService.organizations$(userId).pipe(
getOrganizationById(orgId),
map((org) => this.i18nService.t("atRiskPasswordsDescSingleOrg", org?.name, tasks.length)),
map((org) =>
this.i18nService.t(
tasks.length === 1
? "atRiskPasswordDescSingleOrg"
: "atRiskPasswordsDescSingleOrgPlural",
org?.name,
tasks.length,
),
),
);
}
return of(this.i18nService.t("atRiskPasswordsDescMultiOrg", tasks.length));
return of(this.i18nService.t("atRiskPasswordsDescMultiOrgPlural", tasks.length));
}),
);
async ngOnInit() {
const { userId } = await firstValueFrom(this.activeUserData$);
const gettingStartedDismissed = await firstValueFrom(
this.atRiskPasswordPageService.isGettingStartedDismissed(userId),
);
if (!gettingStartedDismissed) {
const ref = AtRiskCarouselDialogComponent.open(this.dialogService);
const result = await firstValueFrom(ref.closed);
if (result === AtRiskCarouselDialogResult.Dismissed) {
await this.atRiskPasswordPageService.dismissGettingStarted(userId);
}
}
}
async viewCipher(cipher: CipherView) {
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
if (!repromptPassed) {

View File

@@ -36,32 +36,46 @@
<ng-template #loginCopyMenu>
<bit-item-action>
<button
*ngIf="singleCopiableLogin"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
'copyFieldValue' | i18n: singleCopiableLogin.key : singleCopiableLogin.value
"
[disabled]="!hasLoginValues"
[bitMenuTriggerFor]="loginOptions"
[appCopyClick]="singleCopiableLogin.value"
[valueLabel]="singleCopiableLogin.key"
showToast
></button>
<bit-menu #loginOptions>
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
{{ "copyUsername" | i18n }}
</button>
<ng-container *ngIf="!singleCopiableLogin">
<button
*ngIf="cipher.viewPassword"
type="button"
bitMenuItem
appCopyField="password"
[cipher]="cipher"
>
{{ "copyPassword" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="totp" [cipher]="cipher">
{{ "copyVerificationCode" | i18n }}
</button>
</bit-menu>
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasLoginValues"
[bitMenuTriggerFor]="loginOptions"
></button>
<bit-menu #loginOptions>
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
{{ "copyUsername" | i18n }}
</button>
<button
*ngIf="cipher.viewPassword"
type="button"
bitMenuItem
appCopyField="password"
[cipher]="cipher"
>
{{ "copyPassword" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="totp" [cipher]="cipher">
{{ "copyVerificationCode" | i18n }}
</button>
</bit-menu>
</ng-container>
</bit-item-action>
</ng-template>
</ng-container>
@@ -92,52 +106,78 @@
<ng-template #cardCopyMenu>
<bit-item-action>
<button
*ngIf="singleCopiableCard"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasCardValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasCardValues"
[bitMenuTriggerFor]="cardOptions"
[appA11yTitle]="'copyFieldValue' | i18n: singleCopiableCard.key : singleCopiableCard.value"
[appCopyClick]="singleCopiableCard.value"
[valueLabel]="singleCopiableCard.key"
showToast
></button>
<bit-menu #cardOptions>
<button type="button" bitMenuItem appCopyField="cardNumber" [cipher]="cipher">
{{ "copyNumber" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="securityCode" [cipher]="cipher">
{{ "copySecurityCode" | i18n }}
</button>
</bit-menu>
<ng-container *ngIf="!singleCopiableCard">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasCardValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasCardValues"
[bitMenuTriggerFor]="cardOptions"
></button>
<bit-menu #cardOptions>
<button type="button" bitMenuItem appCopyField="cardNumber" [cipher]="cipher">
{{ "copyNumber" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="securityCode" [cipher]="cipher">
{{ "copySecurityCode" | i18n }}
</button>
</bit-menu>
</ng-container>
</bit-item-action>
</ng-template>
</ng-container>
<bit-item-action *ngIf="cipher.type === CipherType.Identity">
<button
*ngIf="singleCopiableIdentity"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasIdentityValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
'copyFieldValue' | i18n: singleCopiableIdentity.key : singleCopiableIdentity.value
"
[disabled]="!hasIdentityValues"
[bitMenuTriggerFor]="identityOptions"
[appCopyClick]="singleCopiableIdentity.value"
[valueLabel]="singleCopiableIdentity.key"
showToast
></button>
<bit-menu #identityOptions>
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
{{ "copyUsername" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="email" [cipher]="cipher">
{{ "copyEmail" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="phone" [cipher]="cipher">
{{ "copyPhone" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="address" [cipher]="cipher">
{{ "copyAddress" | i18n }}
</button>
</bit-menu>
<ng-container *ngIf="!singleCopiableIdentity">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasIdentityValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasIdentityValues"
[bitMenuTriggerFor]="identityOptions"
></button>
<bit-menu #identityOptions>
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
{{ "copyUsername" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="email" [cipher]="cipher">
{{ "copyEmail" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="phone" [cipher]="cipher">
{{ "copyPhone" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="address" [cipher]="cipher">
{{ "copyAddress" | i18n }}
</button>
</bit-menu>
</ng-container>
</bit-item-action>
<bit-item-action *ngIf="cipher.type === CipherType.SecureNote">

View File

@@ -4,6 +4,7 @@ import { CommonModule } from "@angular/common";
import { Component, Input, inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components";
@@ -11,6 +12,11 @@ import { CopyCipherFieldDirective } from "@bitwarden/vault";
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
type CipherItem = {
value: string;
key: string;
};
@Component({
standalone: true,
selector: "app-item-copy-actions",
@@ -37,6 +43,50 @@ export class ItemCopyActionsComponent {
);
}
get singleCopiableLogin() {
const loginItems: CipherItem[] = [
{ value: this.cipher.login.username, key: "username" },
{ value: this.cipher.login.password, key: "password" },
{ value: this.cipher.login.totp, key: "totp" },
];
// If both the password and username are visible but the password is hidden, return the username
if (!this.cipher.viewPassword && this.cipher.login.username && this.cipher.login.password) {
return { value: this.cipher.login.username, key: this.i18nService.t("username") };
}
return this.findSingleCopiableItem(loginItems);
}
get singleCopiableCard() {
const cardItems: CipherItem[] = [
{ value: this.cipher.card.code, key: "code" },
{ value: this.cipher.card.number, key: "number" },
];
return this.findSingleCopiableItem(cardItems);
}
get singleCopiableIdentity() {
const identityItems: CipherItem[] = [
{ value: this.cipher.identity.fullAddressForCopy, key: "address" },
{ value: this.cipher.identity.email, key: "email" },
{ value: this.cipher.identity.username, key: "username" },
{ value: this.cipher.identity.phone, key: "phone" },
];
return this.findSingleCopiableItem(identityItems);
}
/*
* Given a list of CipherItems, if there is only one item with a value,
* return it with the translated key. Otherwise return null
*/
findSingleCopiableItem(items: { value: string; key: string }[]): CipherItem | null {
const singleItemWithValue = items.find(
(key) => key.value && items.every((f) => f === key || !f.value),
);
return singleItemWithValue
? { value: singleItemWithValue.value, key: this.i18nService.t(singleItemWithValue.key) }
: null;
}
get hasCardValues() {
return !!this.cipher.card.code || !!this.cipher.card.number;
}
@@ -62,5 +112,5 @@ export class ItemCopyActionsComponent {
);
}
constructor() {}
constructor(private i18nService: I18nService) {}
}

View File

@@ -0,0 +1,28 @@
<ng-container *ngIf="showNewCustomizationSettingsCallout">
<button
type="button"
class="tw-absolute tw-bottom-[12px] tw-right-[47px]"
[bitPopoverTriggerFor]="newCustomizationOptionsCallout"
[position]="'above-end'"
[popoverOpen]="true"
#triggerRef="popoverTrigger"
></button>
<bit-popover
[title]="'newCustomizationOptionsCalloutTitle' | i18n"
#newCustomizationOptionsCallout
(closed)="dismissCallout()"
>
<div bitTypography="body2" (click)="goToAppearance()">
{{ "newCustomizationOptionsCalloutContent" | i18n }}
<a
tabIndex="0"
bitLink
linkType="primary"
routerLink="/appearance"
(keydown.enter)="goToAppearance()"
>
{{ "newCustomizationOptionsCalloutLink" | i18n }}
</a>
</div>
</bit-popover>
</ng-container>

View File

@@ -0,0 +1,81 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { ButtonModule, PopoverModule } from "@bitwarden/components";
import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
import { VaultPageService } from "../vault-page.service";
@Component({
selector: "new-settings-callout",
templateUrl: "new-settings-callout.component.html",
standalone: true,
imports: [PopoverModule, JslibModule, CommonModule, ButtonModule],
providers: [VaultPageService],
})
export class NewSettingsCalloutComponent implements OnInit, OnDestroy {
protected showNewCustomizationSettingsCallout = false;
protected activeUserId: UserId | null = null;
constructor(
private accountService: AccountService,
private vaultProfileService: VaultProfileService,
private vaultPageService: VaultPageService,
private router: Router,
private logService: LogService,
private copyButtonService: VaultPopupCopyButtonsService,
private vaultSettingsService: VaultSettingsService,
) {}
async ngOnInit() {
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const showQuickCopyActions = await firstValueFrom(this.copyButtonService.showQuickCopyActions$);
const clickItemsToAutofillVaultView = await firstValueFrom(
this.vaultSettingsService.clickItemsToAutofillVaultView$,
);
let profileCreatedDate: Date;
try {
profileCreatedDate = await this.vaultProfileService.getProfileCreationDate(this.activeUserId);
} catch (e) {
this.logService.error("Error getting profile creation date", e);
// Default to before the cutoff date to ensure the callout is shown
profileCreatedDate = new Date("2024-12-24");
}
const hasCalloutBeenDismissed = await firstValueFrom(
this.vaultPageService.isCalloutDismissed(this.activeUserId),
);
this.showNewCustomizationSettingsCallout =
!showQuickCopyActions &&
!clickItemsToAutofillVaultView &&
!hasCalloutBeenDismissed &&
profileCreatedDate < new Date("2024-12-25");
}
async goToAppearance() {
await this.router.navigate(["/appearance"]);
}
async dismissCallout() {
if (this.activeUserId) {
await this.vaultPageService.dismissCallout(this.activeUserId);
}
}
async ngOnDestroy() {
await this.dismissCallout();
}
}

View File

@@ -0,0 +1,35 @@
import { inject, Injectable } from "@angular/core";
import { map, Observable } from "rxjs";
import {
BANNERS_DISMISSED_DISK,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
export const NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY = new UserKeyDefinition<boolean>(
BANNERS_DISMISSED_DISK,
"newCustomizationOptionsCalloutDismissed",
{
deserializer: (calloutDismissed) => calloutDismissed,
clearOn: [], // Do not clear dismissed callouts
},
);
@Injectable()
export class VaultPageService {
private stateProvider = inject(StateProvider);
isCalloutDismissed(userId: UserId): Observable<boolean> {
return this.stateProvider
.getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY)
.state$.pipe(map((dismissed) => !!dismissed));
}
async dismissCallout(userId: UserId): Promise<void> {
await this.stateProvider
.getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY)
.update(() => true);
}
}

View File

@@ -85,4 +85,5 @@
></app-vault-list-items-container>
</div>
</ng-container>
<new-settings-callout></new-settings-callout>
</popup-page>

View File

@@ -15,6 +15,7 @@ import {
} from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -44,7 +45,9 @@ import {
NewItemDropdownV2Component,
NewItemInitialValues,
} from "./new-item-dropdown/new-item-dropdown-v2.component";
import { NewSettingsCalloutComponent } from "./new-settings-callout/new-settings-callout.component";
import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
import { VaultPageService } from "./vault-page.service";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
@@ -77,7 +80,9 @@ enum VaultState {
DecryptionFailureDialogComponent,
BannerComponent,
AtRiskPasswordCalloutComponent,
NewSettingsCalloutComponent,
],
providers: [VaultPageService],
})
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
@@ -115,6 +120,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
protected noResultsIcon = Icons.NoResults;
protected VaultStateEnum = VaultState;
protected showNewCustomizationSettingsCallout = false;
constructor(
private vaultPopupItemsService: VaultPopupItemsService,
@@ -124,6 +130,8 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private destroyRef: DestroyRef,
private cipherService: CipherService,
private dialogService: DialogService,
private vaultProfileService: VaultProfileService,
private vaultPageService: VaultPageService,
) {
combineLatest([
this.vaultPopupItemsService.emptyVault$,
@@ -178,7 +186,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
});
}
ngOnDestroy(): void {
ngOnDestroy() {
this.vaultScrollPositionService.stop();
}

View File

@@ -1,4 +1,4 @@
import "jest-preset-angular/setup-jest";
import "@bitwarden/ui-common/setup-jest";
import { addCustomMatchers } from "@bitwarden/common/spec";
addCustomMatchers();

View File

@@ -32,6 +32,7 @@
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/tools-card": ["../../libs/tools/card/src"],
"@bitwarden/ui-common": ["../../libs/ui/common/src"],
"@bitwarden/ui-common/setup-jest": ["../../libs/ui/common/src/setup-jest"],
"@bitwarden/vault-export-core": [
"../../libs/tools/export/vault-export/vault-export-core/src"
],

View File

@@ -72,7 +72,7 @@
"inquirer": "8.2.6",
"jsdom": "26.0.0",
"jszip": "3.10.1",
"koa": "2.15.3",
"koa": "2.15.4",
"koa-bodyparser": "4.4.1",
"koa-json": "2.0.2",
"lowdb": "1.0.0",

View File

@@ -0,0 +1,44 @@
function SignExe {
param (
[Parameter(Mandatory=$true)]
[ValidateScript({![string]::IsNullOrEmpty($_)})]
[string]$vaultUrl,
[Parameter(Mandatory=$false)]
[ValidateScript({![string]::IsNullOrEmpty($_)})]
[string]$clientId,
[Parameter(Mandatory=$false)]
[ValidateScript({![string]::IsNullOrEmpty($_)})]
[string]$tenantId,
[Parameter(Mandatory=$false)]
[ValidateScript({![string]::IsNullOrEmpty($_)})]
[string]$clientSecret,
[Parameter(Mandatory=$false)]
[ValidateScript({![string]::IsNullOrEmpty($_)})]
[string]$certName,
[Parameter(Mandatory=$false)]
[ValidateScript({Test-Path $_})]
[string] $exePath,
# [Parameter(Mandatory=$false)]
# [string] $hashAlgorithm, # -fd option
# [Parameter(Mandatory=$false)]
# [string] $site, # -du option
[Parameter(Mandatory=$false)]
[string] $timestampService = "http://timestamp.digicert.com"
)
echo "Signing $exePath ..."
azuresigntool sign -kvu $vaultUrl -kvi $clientId -kvt $tenantId -kvs $clientSecret -kvc $certName -tr $timestampService $exePath
}
SignExe -vaultUrl $env:SIGNING_VAULT_URL -clientId $env:SIGNING_CLIENT_ID -tenantId $env:SIGNING_TENANT_ID -clientSecret $env:SIGNING_CLIENT_SECRET -certName $env:SIGNING_CERT_NAME -exePath $env:EXE_PATH

View File

@@ -311,6 +311,26 @@ export class LoginCommand {
);
}
// Opting for not checking feature flag since the server will not respond with
// requiresDeviceVerification if the feature flag is not enabled.
if (response.requiresDeviceVerification) {
let newDeviceToken: string = null;
if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "input",
name: "token",
message: "New device login code:",
});
newDeviceToken = answer.token;
}
if (newDeviceToken == null || newDeviceToken === "") {
return Response.badRequest("Code is required.");
}
response = await this.loginStrategyService.logInNewDeviceVerification(newDeviceToken);
}
if (response.captchaSiteKey) {
const twoFactorRequest = new TokenTwoFactorRequest(selectedProvider.type, twoFactorToken);
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);

View File

@@ -1,3 +1,5 @@
import "core-js/proposals/explicit-resource-management";
import { program } from "commander";
import { OssServeConfigurator } from "./oss-serve-configurator";

View File

@@ -1,7 +1,7 @@
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import * as sdk from "@bitwarden/sdk-internal";
export class CliSdkLoadService implements SdkLoadService {
export class CliSdkLoadService extends SdkLoadService {
async load(): Promise<void> {
const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm");
(sdk as any).init(module);

View File

@@ -867,7 +867,7 @@ export class ServiceContainer {
return;
}
await this.sdkLoadService.load();
await this.sdkLoadService.loadAndInit();
await this.storageService.init();
await this.stateService.init();
this.containerService.attachToGlobal(global);

View File

@@ -333,9 +333,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.85"
version = "0.1.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
dependencies = [
"proc-macro2",
"quote",
@@ -439,7 +439,7 @@ checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "bitwarden-russh"
version = "0.1.0"
source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae#23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae"
source = "git+https://github.com/bitwarden/bitwarden-russh.git?rev=3d48f140fd506412d186203238993163a8c4e536#3d48f140fd506412d186203238993163a8c4e536"
dependencies = [
"anyhow",
"byteorder",
@@ -519,9 +519,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]]
name = "camino"
@@ -635,9 +635,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.27"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
dependencies = [
"clap_builder",
"clap_derive",
@@ -645,9 +645,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.27"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
dependencies = [
"anstream",
"anstyle",
@@ -657,9 +657,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.24"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
dependencies = [
"heck",
"proc-macro2",
@@ -830,9 +830,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.137"
version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc894913dccfed0f84106062c284fa021c3ba70cb1d78797d6f5165d4492e45"
checksum = "8bc580dceb395cae0efdde0a88f034cfd8a276897e40c693a7b87bed17971d33"
dependencies = [
"cc",
"cxxbridge-cmd",
@@ -844,9 +844,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.137"
version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "503b2bfb6b3e8ce7f95d865a67419451832083d3186958290cee6c53e39dfcfe"
checksum = "49d8c1baedad72a7efda12ad8d7ad687b3e7221dfb304a12443fd69e9de8bb30"
dependencies = [
"cc",
"codespan-reporting",
@@ -858,9 +858,9 @@ dependencies = [
[[package]]
name = "cxxbridge-cmd"
version = "1.0.137"
version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0d2cb64a95b4b5a381971482235c4db2e0208302a962acdbe314db03cbbe2fb"
checksum = "e43afb0e3b2ef293492a31ecd796af902112460d53e5f923f7804f348a769f9c"
dependencies = [
"clap",
"codespan-reporting",
@@ -871,15 +871,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.137"
version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f797b0206463c9c2a68ed605ab28892cca784f1ef066050f4942e3de26ad885"
checksum = "0257ad2096a2474fe877e9e055ab69603851c3d6b394efcc7e0443899c2492ce"
[[package]]
name = "cxxbridge-macro"
version = "1.0.137"
version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79010a2093848e65a3e0f7062d3f02fb2ef27f866416dfe436fccfa73d3bb59"
checksum = "b46cbd7358a46b760609f1cb5093683328e58ca50e594a308716f5403fdc03e5"
dependencies = [
"proc-macro2",
"quote",
@@ -942,6 +942,7 @@ dependencies = [
"base64",
"bitwarden-russh",
"byteorder",
"bytes",
"cbc",
"core-foundation",
"desktop_objc",
@@ -1055,15 +1056,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "dlib"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading",
]
[[package]]
name = "doctest-file"
version = "1.0.0"
@@ -1100,9 +1092,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.13.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d"
[[package]]
name = "embed_plist"
@@ -1139,9 +1131,9 @@ dependencies = [
[[package]]
name = "equivalent"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
@@ -1469,9 +1461,9 @@ dependencies = [
[[package]]
name = "inout"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"block-padding",
"generic-array",
@@ -1670,9 +1662,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.3"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [
"adler2",
]
@@ -2007,9 +1999,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.20.2"
version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "oo7"
@@ -2310,9 +2302,9 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.27"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705"
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
dependencies = [
"proc-macro2",
"syn",
@@ -2338,9 +2330,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.36.2"
version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
dependencies = [
"memchr",
]
@@ -2412,9 +2404,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]]
name = "redox_syscall"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
dependencies = [
"bitflags",
]
@@ -2498,9 +2490,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
@@ -2545,12 +2537,6 @@ dependencies = [
"cipher",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -2744,9 +2730,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.13.2"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
[[package]]
name = "smawk"
@@ -2847,9 +2833,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.96"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
@@ -2872,9 +2858,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.16.0"
version = "3.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
dependencies = [
"cfg-if",
"fastrand",
@@ -3043,9 +3029,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.22.22"
version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap",
"toml_datetime",
@@ -3121,9 +3107,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.16"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]]
name = "unicode-segmentation"
@@ -3306,23 +3292,22 @@ dependencies = [
[[package]]
name = "wayland-backend"
version = "0.3.7"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6"
checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
dependencies = [
"cc",
"downcast-rs",
"rustix",
"scoped-tls",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.7"
version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
dependencies = [
"bitflags",
"rustix",
@@ -3357,9 +3342,9 @@ dependencies = [
[[package]]
name = "wayland-scanner"
version = "0.31.5"
version = "0.31.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3"
checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
dependencies = [
"proc-macro2",
"quick-xml",
@@ -3368,12 +3353,10 @@ dependencies = [
[[package]]
name = "wayland-sys"
version = "0.31.5"
version = "0.31.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09"
checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
dependencies = [
"dlib",
"log",
"pkg-config",
]
@@ -3512,6 +3495,12 @@ dependencies = [
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-plugin-authenticator"
version = "0.0.0"
@@ -3525,8 +3514,8 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result 0.3.0",
"windows-strings 0.3.0",
"windows-result 0.3.1",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
@@ -3550,11 +3539,11 @@ dependencies = [
[[package]]
name = "windows-result"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d08106ce80268c4067c0571ca55a9b4e9516518eaa1a1fe9b37ca403ae1d1a34"
checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189"
dependencies = [
"windows-targets 0.53.0",
"windows-link",
]
[[package]]
@@ -3569,11 +3558,11 @@ dependencies = [
[[package]]
name = "windows-strings"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b888f919960b42ea4e11c2f408fadb55f78a9f236d5eef084103c8ce52893491"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-targets 0.53.0",
"windows-link",
]
[[package]]
@@ -3781,9 +3770,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.6.25"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310"
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
dependencies = [
"memchr",
]

View File

@@ -21,7 +21,7 @@ manual_test = []
aes = "=0.8.4"
anyhow = { workspace = true }
arboard = { version = "=3.4.1", default-features = false, features = [
"wayland-data-control",
"wayland-data-control",
] }
argon2 = { version = "=0.5.3", features = ["zeroize"] }
base64 = "=0.22.1"
@@ -39,12 +39,12 @@ scopeguard = "=1.2.0"
sha2 = "=0.10.8"
ssh-encoding = "=0.2.0"
ssh-key = { version = "=0.6.7", default-features = false, features = [
"encryption",
"ed25519",
"rsa",
"getrandom",
"encryption",
"ed25519",
"rsa",
"getrandom",
] }
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae" }
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" }
tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] }
tokio-stream = { workspace = true, features = ["net"] }
tokio-util = { workspace = true, features = ["codec"] }
@@ -53,21 +53,22 @@ typenum = "=1.17.0"
pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] }
rsa = "=0.9.6"
ed25519 = { version = "=2.2.3", features = ["pkcs8"] }
sysinfo = { version = "=0.33.1", features = ["windows"] }
bytes = "1.9.0"
sysinfo = { version = "0.33.1", features = ["windows"] }
[target.'cfg(windows)'.dependencies]
widestring = { version = "=1.1.0", optional = true }
windows = { version = "=0.58.0", features = [
"Foundation",
"Security_Credentials_UI",
"Security_Cryptography",
"Storage_Streams",
"Win32_Foundation",
"Win32_Security_Credentials",
"Win32_System_WinRT",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
"Win32_System_Pipes",
"Foundation",
"Security_Credentials_UI",
"Security_Cryptography",
"Storage_Streams",
"Win32_Foundation",
"Win32_Security_Credentials",
"Win32_System_WinRT",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
"Win32_System_Pipes",
], optional = true }
[target.'cfg(windows)'.dev-dependencies]

View File

@@ -18,6 +18,8 @@ mod peercred_unix_listener_stream;
pub mod importer;
pub mod peerinfo;
mod request_parser;
#[derive(Clone)]
pub struct BitwardenDesktopAgent {
keystore: ssh_agent::KeyStore,
@@ -35,19 +37,37 @@ pub struct SshAgentUIRequest {
pub cipher_id: Option<String>,
pub process_name: String,
pub is_list: bool,
pub namespace: Option<String>,
pub is_forwarding: bool,
}
impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
async fn confirm(&self, ssh_key: Key, info: &peerinfo::models::PeerInfo) -> bool {
async fn confirm(&self, ssh_key: Key, data: &[u8], info: &peerinfo::models::PeerInfo) -> bool {
if !self.is_running() {
println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm");
return false;
}
let request_id = self.get_request_id().await;
let request_data = match request_parser::parse_request(data) {
Ok(data) => data,
Err(e) => {
println!("[SSH Agent] Error while parsing request: {}", e);
return false;
}
};
let namespace = match request_data {
request_parser::SshAgentSignRequest::SshSigRequest(ref req) => {
Some(req.namespace.clone())
}
_ => None,
};
println!(
"[SSH Agent] Confirming request from application: {}",
info.process_name()
"[SSH Agent] Confirming request from application: {}, is_forwarding: {}, namespace: {}",
info.process_name(),
info.is_forwarding(),
namespace.clone().unwrap_or_default(),
);
let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe();
@@ -57,6 +77,8 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
cipher_id: Some(ssh_key.cipher_uuid.clone()),
process_name: info.process_name().to_string(),
is_list: false,
namespace,
is_forwarding: info.is_forwarding(),
})
.await
.expect("Should send request to ui");
@@ -81,6 +103,8 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
cipher_id: None,
process_name: info.process_name().to_string(),
is_list: true,
namespace: None,
is_forwarding: info.is_forwarding(),
};
self.show_ui_request_tx
.send(message)
@@ -93,6 +117,17 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
}
false
}
async fn set_is_forwarding(
&self,
is_forwarding: bool,
connection_info: &peerinfo::models::PeerInfo,
) {
// is_forwarding can only be added but never removed from a connection
if is_forwarding {
connection_info.set_forwarding(is_forwarding);
}
}
}
impl BitwardenDesktopAgent {

View File

@@ -31,26 +31,15 @@ impl Stream for PeercredUnixListenerStream {
Ok(peer) => match peer.pid() {
Some(pid) => pid,
None => {
return Poll::Ready(Some(Err(io::Error::new(
io::ErrorKind::Other,
"Failed to get peer PID",
))));
return Poll::Ready(Some(Ok((stream, PeerInfo::unknown()))));
}
},
Err(err) => {
return Poll::Ready(Some(Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to get peer credentials: {}", err),
))));
}
Err(_) => return Poll::Ready(Some(Ok((stream, PeerInfo::unknown())))),
};
let peer_info = peerinfo::gather::get_peer_info(pid as u32);
match peer_info {
Ok(info) => Poll::Ready(Some(Ok((stream, info)))),
Err(err) => Poll::Ready(Some(Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to get peer info: {}", err),
)))),
Err(_) => Poll::Ready(Some(Ok((stream, PeerInfo::unknown())))),
}
}
Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))),

View File

@@ -1,3 +1,5 @@
use std::sync::{atomic::AtomicBool, Arc};
/**
* Peerinfo represents the information of a peer process connecting over a socket.
* This can be later extended to include more information (icon, app name) for the corresponding application.
@@ -7,6 +9,7 @@ pub struct PeerInfo {
uid: u32,
pid: u32,
process_name: String,
is_forwarding: Arc<AtomicBool>,
}
impl PeerInfo {
@@ -15,6 +18,16 @@ impl PeerInfo {
uid,
pid,
process_name,
is_forwarding: Arc::new(AtomicBool::new(false)),
}
}
pub fn unknown() -> Self {
Self {
uid: 0,
pid: 0,
process_name: "Unknown application".to_string(),
is_forwarding: Arc::new(AtomicBool::new(false)),
}
}
@@ -29,4 +42,14 @@ impl PeerInfo {
pub fn process_name(&self) -> &str {
&self.process_name
}
pub fn is_forwarding(&self) -> bool {
self.is_forwarding
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_forwarding(&self, value: bool) {
self.is_forwarding
.store(value, std::sync::atomic::Ordering::Relaxed);
}
}

View File

@@ -0,0 +1,41 @@
use bytes::{Buf, Bytes};
#[derive(Debug)]
pub(crate) struct SshSigRequest {
pub namespace: String,
}
#[derive(Debug)]
pub(crate) struct SignRequest {}
#[derive(Debug)]
pub(crate) enum SshAgentSignRequest {
SshSigRequest(SshSigRequest),
SignRequest(SignRequest),
}
pub(crate) fn parse_request(data: &[u8]) -> Result<SshAgentSignRequest, anyhow::Error> {
let mut data = Bytes::copy_from_slice(data);
let magic_header = "SSHSIG";
let header = data.split_to(magic_header.len());
// sshsig; based on https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
if header == magic_header.as_bytes() {
let _version = data.get_u32();
// read until null byte
let namespace = data
.into_iter()
.take_while(|&x| x != 0)
.collect::<Vec<u8>>();
let namespace =
String::from_utf8(namespace).map_err(|_| anyhow::anyhow!("Invalid namespace"))?;
Ok(SshAgentSignRequest::SshSigRequest(SshSigRequest {
namespace,
}))
} else {
// regular sign request
Ok(SshAgentSignRequest::SignRequest(SignRequest {}))
}
}

View File

@@ -47,11 +47,21 @@ impl BitwardenDesktopAgent {
return;
}
};
ssh_agent_directory
.join(".bitwarden-ssh-agent.sock")
.to_str()
.expect("Path should be valid")
.to_owned()
let is_flatpak = std::env::var("container") == Ok("flatpak".to_string());
if !is_flatpak {
ssh_agent_directory
.join(".bitwarden-ssh-agent.sock")
.to_str()
.expect("Path should be valid")
.to_owned()
} else {
ssh_agent_directory
.join(".var/app/com.bitwarden.desktop/data/.bitwarden-ssh-agent.sock")
.to_str()
.expect("Path should be valid")
.to_owned()
}
}
};

View File

@@ -67,7 +67,14 @@ export declare namespace sshagent {
status: SshKeyImportStatus
sshKey?: SshKey
}
export function serve(callback: (err: Error | null, arg0: string | undefined | null, arg1: boolean, arg2: string) => any): Promise<SshAgentState>
export interface SshUiRequest {
cipherId?: string
isList: boolean
processName: string
isForwarding: boolean
namespace?: string
}
export function serve(callback: (err: Error | null, arg: SshUiRequest) => any): Promise<SshAgentState>
export function stop(agentState: SshAgentState): void
export function isRunning(agentState: SshAgentState): boolean
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void

View File

@@ -243,9 +243,18 @@ pub mod sshagent {
}
}
#[napi(object)]
pub struct SshUIRequest {
pub cipher_id: Option<String>,
pub is_list: bool,
pub process_name: String,
pub is_forwarding: bool,
pub namespace: Option<String>,
}
#[napi]
pub async fn serve(
callback: ThreadsafeFunction<(Option<String>, bool, String), CalleeHandled>,
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
) -> napi::Result<SshAgentState> {
let (auth_request_tx, mut auth_request_rx) =
tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32);
@@ -262,11 +271,13 @@ pub mod sshagent {
let auth_response_tx_arc = cloned_response_tx_arc;
let callback = cloned_callback;
let promise_result: Result<Promise<bool>, napi::Error> = callback
.call_async(Ok((
request.cipher_id,
request.is_list,
request.process_name,
)))
.call_async(Ok(SshUIRequest {
cipher_id: request.cipher_id,
is_list: request.is_list,
process_name: request.process_name,
is_forwarding: request.is_forwarding,
namespace: request.namespace,
}))
.await;
match promise_result {
Ok(promise_result) => match promise_result.await {

View File

@@ -12,15 +12,13 @@
"@bitwarden/common": "file:../../../libs/common",
"@bitwarden/node": "file:../../../libs/node",
"module-alias": "2.2.3",
"node-ipc": "9.2.1",
"ts-node": "10.9.2",
"uuid": "11.0.5",
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "22.10.7",
"@types/node-ipc": "9.2.3",
"typescript": "4.7.4"
"typescript": "5.4.2"
}
},
"../../../libs/common": {
@@ -31,10 +29,7 @@
"../../../libs/node": {
"name": "@bitwarden/node",
"version": "0.0.0",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/common": "file:../common"
}
"license": "GPL-3.0"
},
"node_modules/@bitwarden/common": {
"resolved": "../../../libs/common",
@@ -114,16 +109,6 @@
"undici-types": "~6.20.0"
}
},
"node_modules/@types/node-ipc": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz",
"integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
@@ -225,15 +210,6 @@
"node": ">=0.3.1"
}
},
"node_modules/easy-stack": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
"integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -249,15 +225,6 @@
"node": ">=6"
}
},
"node_modules/event-pubsub": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz",
"integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==",
"license": "Unlicense",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -276,27 +243,6 @@
"node": ">=8"
}
},
"node_modules/js-message": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
"license": "MIT",
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/js-queue": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz",
"integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==",
"license": "MIT",
"dependencies": {
"easy-stack": "^1.0.1"
},
"engines": {
"node": ">=1.0.0"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -309,20 +255,6 @@
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
"license": "MIT"
},
"node_modules/node-ipc": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz",
"integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==",
"license": "MIT",
"dependencies": {
"event-pubsub": "4.3.0",
"js-message": "1.0.7",
"js-queue": "2.0.2"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -402,16 +334,16 @@
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/undici-types": {

View File

@@ -17,15 +17,13 @@
"@bitwarden/common": "file:../../../libs/common",
"@bitwarden/node": "file:../../../libs/node",
"module-alias": "2.2.3",
"node-ipc": "9.2.1",
"ts-node": "10.9.2",
"uuid": "11.0.5",
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "22.10.7",
"@types/node-ipc": "9.2.3",
"typescript": "4.7.4"
"typescript": "5.4.2"
},
"_moduleAliases": {
"@bitwarden/common": "dist/libs/common/src",

View File

@@ -1,9 +1,7 @@
/* eslint-disable no-console */
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { homedir } from "os";
import * as NodeIPC from "node-ipc";
import { ChildProcess, spawn } from "child_process";
import * as fs from "fs";
import * as path from "path";
// eslint-disable-next-line no-restricted-imports
import { MessageCommon } from "../../src/models/native-messaging/message-common";
@@ -13,11 +11,6 @@ import { UnencryptedMessageResponse } from "../../src/models/native-messaging/un
import Deferred from "./deferred";
import { race } from "./race";
NodeIPC.config.id = "native-messaging-test-runner";
NodeIPC.config.maxRetries = 0;
NodeIPC.config.silent = true;
const DESKTOP_APP_PATH = `${homedir}/tmp/app.bitwarden`;
const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds
export type MessageHandler = (MessageCommon) => void;
@@ -42,6 +35,10 @@ export default class IPCService {
// A set of deferred promises that are awaiting socket connection
private awaitingConnection = new Set<Deferred<void>>();
// The IPC desktop_proxy process
private process?: ChildProcess;
private processOutputBuffer = Buffer.alloc(0);
constructor(
private socketName: string,
private messageHandler: MessageHandler,
@@ -72,47 +69,47 @@ export default class IPCService {
private _connect() {
this.connectionState = IPCConnectionState.Connecting;
NodeIPC.connectTo(this.socketName, DESKTOP_APP_PATH, () => {
// Process incoming message
this.getSocket().on("message", (message: any) => {
this.processMessage(message);
});
const proxyPath = selectProxyPath();
console.log(`[IPCService] connecting to proxy at ${proxyPath}`);
this.getSocket().on("error", (error: Error) => {
// Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
// invoked multiple times each time a connection error happens
console.log("[IPCService] errored");
console.log(
"\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m",
);
this.awaitingConnection.forEach((deferred) => {
console.log(`rejecting: ${deferred}`);
deferred.reject(error);
});
this.awaitingConnection.clear();
});
this.process = spawn(proxyPath, process.argv.slice(1), {
cwd: process.cwd(),
stdio: "pipe",
shell: false,
});
this.getSocket().on("connect", () => {
console.log("[IPCService] connected");
this.connectionState = IPCConnectionState.Connected;
this.process.stdout.on("data", (data: Buffer) => {
this.processIpcMessage(data);
});
this.awaitingConnection.forEach((deferred) => {
deferred.resolve(null);
});
this.awaitingConnection.clear();
});
this.process.stderr.on("data", (data: Buffer) => {
console.error(`proxy log: ${data}`);
});
this.getSocket().on("disconnect", () => {
console.log("[IPCService] disconnected");
this.connectionState = IPCConnectionState.Disconnected;
this.process.on("error", (error) => {
// Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
// invoked multiple times each time a connection error happens
console.log("[IPCService] errored");
console.log(
"\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m",
);
this.awaitingConnection.forEach((deferred) => {
console.log(`rejecting: ${deferred}`);
deferred.reject(error);
});
this.awaitingConnection.clear();
});
this.process.on("exit", () => {
console.log("[IPCService] disconnected");
this.connectionState = IPCConnectionState.Disconnected;
});
}
disconnect() {
console.log("[IPCService] disconnecting...");
if (this.connectionState !== IPCConnectionState.Disconnected) {
NodeIPC.disconnect(this.socketName);
this.process?.kill();
}
}
@@ -133,7 +130,7 @@ export default class IPCService {
this.pendingMessages.set(message.messageId, deferred);
this.getSocket().emit("message", message);
this.sendIpcMessage(message);
try {
// Since we can not guarantee that a response message will ever be sent, we put a timeout
@@ -151,8 +148,56 @@ export default class IPCService {
}
}
private getSocket() {
return NodeIPC.of[this.socketName];
// As we're using the desktop_proxy to communicate with the native messaging directly,
// the messages need to follow Native Messaging Host protocol (uint32 size followed by message).
// https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol
private sendIpcMessage(message: MessageCommon) {
const messageStr = JSON.stringify(message);
const buffer = Buffer.alloc(4 + messageStr.length);
buffer.writeUInt32LE(messageStr.length, 0);
buffer.write(messageStr, 4);
this.process?.stdin.write(buffer);
}
private processIpcMessage(data: Buffer) {
this.processOutputBuffer = Buffer.concat([this.processOutputBuffer, data]);
// We might receive more than one IPC message per data event, so we need to process them all
// We continue as long as we have at least 4 + 1 bytes in the buffer, where the first 4 bytes
// represent the message length and the 5th byte is the message
while (this.processOutputBuffer.length > 4) {
// Read the message length and ensure we have the full message
const msgLength = this.processOutputBuffer.readUInt32LE(0);
if (msgLength + 4 < this.processOutputBuffer.length) {
return;
}
// Parse the message from the buffer
const messageStr = this.processOutputBuffer.subarray(4, msgLength + 4).toString();
const message = JSON.parse(messageStr);
// Store the remaining buffer, which is part of the next message
this.processOutputBuffer = this.processOutputBuffer.subarray(msgLength + 4);
// Process the connect/disconnect messages separately
if (message?.command === "connected") {
console.log("[IPCService] connected");
this.connectionState = IPCConnectionState.Connected;
this.awaitingConnection.forEach((deferred) => {
deferred.resolve(null);
});
this.awaitingConnection.clear();
continue;
} else if (message?.command === "disconnected") {
console.log("[IPCService] disconnected");
this.connectionState = IPCConnectionState.Disconnected;
continue;
}
this.processMessage(message);
}
}
private processMessage(message: any) {
@@ -172,3 +217,41 @@ export default class IPCService {
}
}
}
function selectProxyPath(): string {
const proxyExtension = process.platform === "win32" ? ".exe" : "";
// If the PROXY_PATH environment variable is set, use that
if (process.env.PROXY_PATH) {
if (!fs.existsSync(process.env.PROXY_PATH)) {
throw new Error(`PROXY_PATH is set to ${process.env.PROXY_PATH} but the file does not exist`);
}
return process.env.PROXY_PATH;
}
// Otherwise try the debug build if present
const debugProxyPath = path.join(
__dirname,
"..",
"..",
"..",
"..",
"..",
"..",
"desktop_native",
"target",
"debug",
`desktop_proxy${proxyExtension}`,
);
if (fs.existsSync(debugProxyPath)) {
return debugProxyPath;
}
// On MacOS, try the release build (sandboxed)
const macReleaseProxyPath = `/Applications/Bitwarden.app/Contents/MacOS/desktop_proxy${proxyExtension}`;
if (process.platform === "darwin" && fs.existsSync(macReleaseProxyPath)) {
return macReleaseProxyPath;
}
throw new Error("Could not find the desktop_proxy executable");
}

View File

@@ -5,12 +5,17 @@
"target": "es6",
"module": "CommonJS",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": false,
"declaration": false,
"paths": {
"@src/*": ["src/*"],
"@bitwarden/node/*": ["../../../libs/node/src/*"],
"@bitwarden/common/*": ["../../../libs/common/src/*"]
"@bitwarden/admin-console/*": ["../../../libs/admin-console/src/*"],
"@bitwarden/auth/*": ["../../../libs/auth/src/*"],
"@bitwarden/common/*": ["../../../libs/common/src/*"],
"@bitwarden/key-management": ["../../../libs/key-management/src/"],
"@bitwarden/node/*": ["../../../libs/node/src/*"]
},
"plugins": [
{

View File

@@ -15,7 +15,6 @@ import {
tdeDecryptionRequiredGuard,
unauthGuardFn,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
AnonLayoutWrapperComponent,
@@ -43,7 +42,6 @@ import {
NewDeviceVerificationComponent,
DeviceVerificationIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LockComponent } from "@bitwarden/key-management-ui";
import {
NewDeviceVerificationNoticePageOneComponent,
@@ -123,11 +121,7 @@ const routes: Routes = [
{
path: "device-verification",
component: AnonLayoutWrapperComponent,
canActivate: [
canAccessFeature(FeatureFlag.NewDeviceVerification),
unauthGuardFn(),
activeAuthGuard(),
],
canActivate: [unauthGuardFn(), activeAuthGuard()],
children: [{ path: "", component: NewDeviceVerificationComponent }],
data: {
pageIcon: DeviceVerificationIcon,

View File

@@ -110,7 +110,9 @@ export class AccountSwitcherComponent implements OnInit {
name: active.name,
email: active.email,
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
server: (await this.environmentService.getEnvironment())?.getHostname(),
server: (
await firstValueFrom(this.environmentService.getEnvironment$(active.id))
)?.getHostname(),
};
}),
);
@@ -221,7 +223,9 @@ export class AccountSwitcherComponent implements OnInit {
email: baseAccounts[userId].email,
authenticationStatus: await this.authService.getAuthStatus(userId),
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
server: (await this.environmentService.getEnvironment(userId))?.getHostname(),
server: (
await firstValueFrom(this.environmentService.getEnvironment$(userId as UserId))
)?.getHostname(),
};
}

View File

@@ -1,3 +1,5 @@
import "core-js/proposals/explicit-resource-management";
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

View File

@@ -54,7 +54,7 @@ export class InitService {
init() {
return async () => {
await this.sdkLoadService.load();
await this.sdkLoadService.loadAndInit();
await this.sshAgentService.init();
this.nativeMessagingService.init();
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process

View File

@@ -3509,9 +3509,27 @@
"sshkeyApprovalTitle": {
"message": "Confirm SSH key usage"
},
"agentForwardingWarningTitle": {
"message": "Warning: Agent Forwarding"
},
"agentForwardingWarningText": {
"message": "This request comes from a remote device that you are logged into"
},
"sshkeyApprovalMessageInfix": {
"message": "is requesting access to"
},
"sshkeyApprovalMessageSuffix": {
"message": "in order to"
},
"sshActionLogin": {
"message": "authenticate to a server"
},
"sshActionSign": {
"message": "sign a message"
},
"sshActionGitSign": {
"message": "sign a git commit"
},
"unknownApplication": {
"message": "An application"
},

View File

@@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import "core-js/proposals/explicit-resource-management";
import * as path from "path";
import { app } from "electron";

View File

@@ -2,8 +2,17 @@
<bit-dialog>
<div class="tw-font-semibold" bitDialogTitle>{{ "sshkeyApprovalTitle" | i18n }}</div>
<div bitDialogContent>
<app-callout
type="warning"
title="{{ 'agentForwardingWarningTitle' | i18n }}"
*ngIf="params.isAgentForwarding"
>
{{ 'agentForwardingWarningText' | i18n }}
</app-callout>
<b>{{params.applicationName}}</b> {{ "sshkeyApprovalMessageInfix" | i18n }}
<b>{{params.cipherName}}</b>.
<b>{{params.cipherName}}</b>
{{ "sshkeyApprovalMessageSuffix" | i18n }} {{ params.action | i18n }}
</div>
<div bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary">

View File

@@ -17,6 +17,8 @@ import { CipherFormGeneratorComponent } from "@bitwarden/vault";
export interface ApproveSshRequestParams {
cipherName: string;
applicationName: string;
isAgentForwarding: boolean;
action: string;
}
@Component({
@@ -44,11 +46,26 @@ export class ApproveSshRequestComponent {
private formBuilder: FormBuilder,
) {}
static open(dialogService: DialogService, cipherName: string, applicationName: string) {
static open(
dialogService: DialogService,
cipherName: string,
applicationName: string,
isAgentForwarding: boolean,
namespace: string,
) {
let actioni18nKey = "sshActionLogin";
if (namespace === "git") {
actioni18nKey = "sshActionGitSign";
} else if (namespace != null && namespace != "") {
actioni18nKey = "sshActionSign";
}
return dialogService.open<boolean, ApproveSshRequestParams>(ApproveSshRequestComponent, {
data: {
cipherName,
applicationName,
isAgentForwarding,
action: actioni18nKey,
},
});
}

View File

@@ -47,7 +47,7 @@ export class MainSshAgentService {
init() {
// handle sign request passing to UI
sshagent
.serve(async (err: Error, cipherId: string, isListRequest: boolean, processName: string) => {
.serve(async (err: Error, sshUiRequest: sshagent.SshUiRequest) => {
// clear all old (> SIGN_TIMEOUT) requests
this.requestResponses = this.requestResponses.filter(
(response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT),
@@ -56,10 +56,12 @@ export class MainSshAgentService {
this.request_id += 1;
const id_for_this_request = this.request_id;
this.messagingService.send("sshagent.signrequest", {
cipherId,
isListRequest,
cipherId: sshUiRequest.cipherId,
isListRequest: sshUiRequest.isList,
requestId: id_for_this_request,
processName,
processName: sshUiRequest.processName,
isAgentForwarding: sshUiRequest.isForwarding,
namespace: sshUiRequest.namespace,
});
const result = await firstValueFrom(

View File

@@ -148,6 +148,8 @@ export class SshAgentService implements OnDestroy {
const isListRequest = message.isListRequest as boolean;
const requestId = message.requestId as number;
let application = message.processName as string;
const namespace = message.namespace as string;
const isAgentForwarding = message.isAgentForwarding as boolean;
if (application == "") {
application = this.i18nService.t("unknownApplication");
}
@@ -181,6 +183,8 @@ export class SshAgentService implements OnDestroy {
this.dialogService,
cipher.name,
application,
isAgentForwarding,
namespace,
);
const result = await firstValueFrom(dialogRef.closed);

View File

@@ -1,4 +1,4 @@
import "jest-preset-angular/setup-jest";
import "@bitwarden/ui-common/setup-jest";
Object.defineProperty(window, "CSS", { value: null });
Object.defineProperty(window, "getComputedStyle", {

View File

@@ -31,6 +31,7 @@
"@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"],
"@bitwarden/tools-card": ["../../libs/tools/card/src"],
"@bitwarden/ui-common": ["../../libs/ui/common/src"],
"@bitwarden/ui-common/setup-jest": ["../../libs/ui/common/src/setup-jest"],
"@bitwarden/vault-export-core": [
"../../libs/tools/export/vault-export/vault-export-core/src"
],

View File

@@ -40,7 +40,10 @@
<bit-nav-item
[text]="'eventLogs' | i18n"
route="reporting/events"
*ngIf="organization.canAccessEventLogs"
*ngIf="
(organization.canAccessEventLogs && organization.useEvents) ||
(organization.isOwner && (isBreadcrumbEventLogsEnabled$ | async))
"
></bit-nav-item>
<bit-nav-item
[text]="'reports' | i18n"

View File

@@ -65,6 +65,7 @@ export class OrganizationLayoutComponent implements OnInit {
enterpriseOrganization$: Observable<boolean>;
showAccountDeprovisioningBanner$: Observable<boolean>;
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
@@ -78,6 +79,9 @@ export class OrganizationLayoutComponent implements OnInit {
) {}
async ngOnInit() {
this.isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.PM12276_BreadcrumbEventLogs,
);
document.body.classList.remove("layout_frontend");
this.organization$ = this.route.params.pipe(

View File

@@ -1,5 +1,9 @@
<app-header></app-header>
@let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async);
<app-header>
<span bitBadge variant="primary" slot="title-suffix" *ngIf="usePlaceHolderEvents">
{{ "upgrade" | i18n }}
</span>
</app-header>
<div class="tw-mb-4" [formGroup]="eventsForm">
<div class="tw-mt-4 tw-flex tw-items-center">
<bit-form-field>
@@ -31,6 +35,7 @@
bitFormButton
buttonType="primary"
[bitAction]="refreshEvents"
[disabled]="usePlaceHolderEvents"
>
{{ "update" | i18n }}
</button>
@@ -42,7 +47,7 @@
bitButton
bitFormButton
[bitAction]="exportEvents"
[disabled]="dirtyDates"
[disabled]="dirtyDates || usePlaceHolderEvents"
>
<span>{{ "export" | i18n }}</span>
<i class="bwi bwi-fw bwi-sign-in" aria-hidden="true"></i>
@@ -50,6 +55,13 @@
</form>
</div>
</div>
<bit-callout
type="info"
[title]="'upgradeEventLogTitle' | i18n"
*ngIf="loaded && usePlaceHolderEvents"
>
{{ "upgradeEventLogMessage" | i18n }}
</bit-callout>
<ng-container *ngIf="!loaded">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
@@ -59,8 +71,10 @@
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="loaded">
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p>
<bit-table *ngIf="events && events.length">
@let displayedEvents = organization?.useEvents ? events : placeholderEvents;
<p *ngIf="!displayedEvents || !displayedEvents.length">{{ "noEventsInList" | i18n }}</p>
<bit-table *ngIf="displayedEvents && displayedEvents.length">
<ng-container header>
<tr>
<th bitCell>{{ "timestamp" | i18n }}</th>
@@ -70,8 +84,10 @@
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let e of events" alignContent="top">
<td bitCell class="tw-whitespace-nowrap">{{ e.date | date: "medium" }}</td>
<tr bitRow *ngFor="let e of displayedEvents; index as i" alignContent="top">
<td bitCell class="tw-whitespace-nowrap">
{{ i > 4 && usePlaceHolderEvents ? "******" : (e.date | date: "medium") }}
</td>
<td bitCell>
<span title="{{ e.appName }}, {{ e.ip }}">{{ e.appName }}</span>
</td>
@@ -92,3 +108,26 @@
{{ "loadMore" | i18n }}
</button>
</ng-container>
<ng-container *ngIf="loaded && usePlaceHolderEvents">
<div
class="tw-relative tw--top-72 tw-bg-[#ffffff] tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center"
>
<div
class="tw-bg-[#ffffff] tw-max-w-xl tw-flex-col tw-justify-center tw-text-center tw-p-5 tw-px-10 tw-rounded tw-border-0 tw-border-b tw-border-secondary-300 tw-border-solid mt-5"
>
<i class="bwi bwi-2x bwi-business text-primary"></i>
<p class="tw-font-bold mt-2">
{{ "limitedEventLogs" | i18n: ProductTierType[organization?.productTierType] }}
</p>
<p>
{{ "upgradeForFullEvents" | i18n }}
</p>
<button type="button" class="tw-mt-1" bitButton buttonType="primary" (click)="changePlan()">
{{ "changeBillingPlan" | i18n }}
</button>
</div>
</div>
</ng-container>

View File

@@ -2,11 +2,12 @@
// @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs";
import { concatMap, firstValueFrom, lastValueFrom, Subject, takeUntil } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import {
getOrganizationById,
OrganizationService,
@@ -15,18 +16,29 @@ import { ProviderService } from "@bitwarden/common/admin-console/abstractions/pr
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
import { EventSystemUser } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EventResponse } from "@bitwarden/common/models/response/event.response";
import { EventView } from "@bitwarden/common/models/view/event.view";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.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";
import { ToastService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import {
ChangePlanDialogResultType,
openChangePlanDialog,
} from "../../../billing/organizations/change-plan-dialog.component";
import { EventService } from "../../../core";
import { EventExportService } from "../../../tools/event-export";
import { BaseEventsComponent } from "../../common/base.events.component";
import { placeholderEvents } from "./placeholder-events";
const EVENT_SYSTEM_USER_TO_TRANSLATION: Record<EventSystemUser, string> = {
[EventSystemUser.SCIM]: null, // SCIM acronym not able to be translated so just display SCIM
[EventSystemUser.DomainVerification]: "domainVerification",
@@ -41,10 +53,19 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
exportFileName = "org-events";
organizationId: string;
organization: Organization;
organizationSubscription: OrganizationSubscriptionResponse;
placeholderEvents = placeholderEvents as EventView[];
private orgUsersUserIdMap = new Map<string, any>();
private destroy$ = new Subject<void>();
readonly ProductTierType = ProductTierType;
protected isBreadcrumbEventLogsEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.PM12276_BreadcrumbEventLogs,
);
constructor(
private apiService: ApiService,
private route: ActivatedRoute,
@@ -57,10 +78,13 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
private userNamePipe: UserNamePipe,
private organizationService: OrganizationService,
private organizationUserApiService: OrganizationUserApiService,
private organizationApiService: OrganizationApiServiceAbstraction,
private providerService: ProviderService,
fileDownloadService: FileDownloadService,
toastService: ToastService,
private accountService: AccountService,
private dialogService: DialogService,
private configService: ConfigService,
) {
super(
eventService,
@@ -84,10 +108,16 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
.organizations$(userId)
.pipe(getOrganizationById(this.organizationId)),
);
if (this.organization == null || !this.organization.useEvents) {
await this.router.navigate(["/organizations", this.organizationId]);
return;
if (!this.organization.useEvents) {
this.eventsForm.get("start").disable();
this.eventsForm.get("end").disable();
this.organizationSubscription = await this.organizationApiService.getSubscription(
this.organizationId,
);
}
await this.load();
}),
takeUntil(this.destroy$),
@@ -126,7 +156,6 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
this.logService.warning(e);
}
}
await this.refreshEvents();
this.loaded = true;
}
@@ -186,6 +215,23 @@ export class EventsComponent extends BaseEventsComponent implements OnInit, OnDe
return id?.substring(0, 8);
}
async changePlan() {
const reference = openChangePlanDialog(this.dialogService, {
data: {
organizationId: this.organizationId,
subscription: this.organizationSubscription,
productTierType: this.organization.productTierType,
},
});
const result = await lastValueFrom(reference.closed);
if (result === ChangePlanDialogResultType.Closed) {
return;
}
await this.load();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();

View File

@@ -0,0 +1,63 @@
function getRandomDateTime() {
const now = new Date();
const past24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const randomTime =
past24Hours.getTime() + Math.random() * (now.getTime() - past24Hours.getTime());
const randomDate = new Date(randomTime);
return randomDate.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
}
const asteriskPlaceholders = new Array(6).fill({
appName: "***",
userName: "**********",
userEmail: "**********",
message: "**********",
});
export const placeholderEvents = [
{
date: getRandomDateTime(),
appName: "Extension - Firefox",
userName: "Alice",
userEmail: "alice@email.com",
message: "Logged in",
},
{
date: getRandomDateTime(),
appName: "Mobile - iOS",
userName: "Bob",
message: `Viewed item <span class="tw-text-code">000000</span>`,
},
{
date: getRandomDateTime(),
appName: "Desktop - Linux",
userName: "Carlos",
userEmail: "carlos@email.com",
message: "Login attempt failed with incorrect password",
},
{
date: getRandomDateTime(),
appName: "Web vault - Chrome",
userName: "Ivan",
userEmail: "ivan@email.com",
message: `Confirmed user <span class="tw-text-code">000000</span>`,
},
{
date: getRandomDateTime(),
appName: "Mobile - Android",
userName: "Franz",
userEmail: "franz@email.com",
message: `Sent item <span class="tw-text-code">000000</span> to trash`,
},
]
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
.concat(asteriskPlaceholders);

View File

@@ -1,12 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { inject, NgModule } from "@angular/core";
import { CanMatchFn, RouterModule, Routes } from "@angular/router";
import { map } from "rxjs";
import { canAccessReportingTab } 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";
/* eslint no-restricted-imports: "off" -- Normally prohibited by Tools Team eslint rules but required here */
import { ExposedPasswordsReportComponent } from "../../../tools/reports/pages/organizations/exposed-passwords-report.component";
import { InactiveTwoFactorReportComponent } from "../../../tools/reports/pages/organizations/inactive-two-factor-report.component";
import { ReusedPasswordsReportComponent } from "../../../tools/reports/pages/organizations/reused-passwords-report.component";
@@ -20,6 +22,11 @@ import { EventsComponent } from "../manage/events.component";
import { ReportsHomeComponent } from "./reports-home.component";
const breadcrumbEventLogsPermission$: CanMatchFn = () =>
inject(ConfigService)
.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs)
.pipe(map((breadcrumbEventLogs) => breadcrumbEventLogs === true));
const routes: Routes = [
{
path: "",
@@ -81,6 +88,20 @@ const routes: Routes = [
},
],
},
// Event routing is temporarily duplicated
{
path: "events",
component: EventsComponent,
canMatch: [breadcrumbEventLogsPermission$], // if this matches, the flag is ON
canActivate: [
organizationPermissionsGuard(
(org) => (org.canAccessEventLogs && org.useEvents) || org.isOwner,
),
],
data: {
titleId: "eventLogs",
},
},
{
path: "events",
component: EventsComponent,

View File

@@ -70,6 +70,8 @@ describe("RecoverTwoFactorComponent", () => {
},
],
imports: [I18nPipe],
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
errorOnUnknownElements: false,
});
fixture = TestBed.createComponent(RecoverTwoFactorComponent);

View File

@@ -1,8 +1,8 @@
<bit-dialog dialogSize="default">
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<span bitTypography="body1">{{ "recoveryCodeTitle" | i18n }}</span>
</span>
<bit-dialog
dialogSize="default"
[title]="'twoStepLogin' | i18n"
[subtitle]="'recoveryCodeTitle' | i18n"
>
<ng-container *ngIf="authed" bitDialogContent>
<ng-container *ngIf="code">
<p bitTypography="body1" class="tw-text-center">{{ "twoFactorRecoveryYourCode" | i18n }}:</p>

View File

@@ -1,9 +1,9 @@
<form *ngIf="authed" [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="default">
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<span bitTypography="body1">{{ "authenticatorAppTitle" | i18n }}</span>
</span>
<bit-dialog
dialogSize="default"
[title]="'twoStepLogin' | i18n"
[subtitle]="'authenticatorAppTitle' | i18n"
>
<ng-container bitDialogContent>
<ng-container *ngIf="enabled">
<bit-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi-check-circle">

View File

@@ -1,9 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="authed" autocomplete="off">
<bit-dialog>
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<span bitTypography="body1">Duo</span>
</span>
<bit-dialog [title]="'twoStepLogin' | i18n" [subtitle]="'Duo'">
<ng-container bitDialogContent>
<ng-container *ngIf="enabled">
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">

View File

@@ -1,9 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="authed">
<bit-dialog>
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<span bitTypography="body1">{{ "emailTitle" | i18n }}</span>
</span>
<bit-dialog [title]="'twoStepLogin' | i18n" [subtitle]="'emailTitle' | i18n">
<ng-container bitDialogContent>
<ng-container *ngIf="enabled">
<bit-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi-check-circle">

View File

@@ -1,9 +1,9 @@
<form *ngIf="authed" [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large">
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<span bitTypography="body1">{{ "webAuthnTitle" | i18n }}</span>
</span>
<bit-dialog
dialogSize="large"
[title]="'twoStepLogin' | i18n"
[subtitle]="'webAuthnTitle' | i18n"
>
<ng-container bitDialogContent>
<app-callout
type="success"

View File

@@ -1,9 +1,5 @@
<form *ngIf="authed" [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large">
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<span bitTypography="body1">YubiKey</span>
</span>
<bit-dialog dialogSize="large" [title]="'twoStepLogin' | i18n" [subtitle]="'YubiKey'">
<ng-container bitDialogContent>
<app-callout
*ngIf="enabled"

View File

@@ -1,9 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="default">
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<small class="tw-text-muted">{{ dialogTitle }}</small>
</span>
<bit-dialog dialogSize="default" [title]="'twoStepLogin' | i18n" [subtitle]="dialogTitle">
<ng-container bitDialogContent>
<app-user-verification-form-input
formControlName="secret"

View File

@@ -1,6 +1,6 @@
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>

View File

@@ -27,7 +27,7 @@
#reinstateBtn
(click)="reinstate()"
[appApiAction]="reinstatePromise"
[disabled]="$any(reinstateBtn).loading"
[disabled]="$any(reinstateBtn).loading()"
>
{{ "reinstateSubscription" | i18n }}
</button>
@@ -109,7 +109,7 @@
class="tw-ml-auto"
(click)="cancelSubscription()"
[appApiAction]="cancelPromise"
[disabled]="$any(cancelBtn).loading"
[disabled]="$any(cancelBtn).loading()"
*ngIf="subscription && !subscription.cancelled && !subscriptionMarkedForCancel"
>
{{ "cancelSubscription" | i18n }}

View File

@@ -55,7 +55,7 @@
class="tw-grid tw-grid-flow-col tw-gap-4 tw-mb-4"
[class]="'tw-grid-cols-' + selectableProducts.length"
>
<div
<bit-card
*ngFor="
let selectableProduct of selectableProducts;
trackBy: manageSelectableProduct;
@@ -64,7 +64,6 @@
[ngClass]="getPlanCardContainerClasses(selectableProduct, i)"
(click)="selectPlan(selectableProduct)"
[attr.tabindex]="focusedIndex !== i || isCardDisabled(i) ? '-1' : '0'"
class="product-card"
(keyup)="onKeydown($event, i)"
(focus)="onFocus(i)"
[attr.aria-disabled]="isCardDisabled(i)"
@@ -322,7 +321,7 @@
</ul>
</ng-template>
</ng-template>
</div>
</bit-card>
</div>
<br />
<bit-callout
@@ -353,7 +352,7 @@
>
<i class="bwi bwi-fw" [ngClass]="paymentSourceClasses"></i>
{{ paymentSource?.description }}
<span class="ml-2 tw-text-primary-600 tw-cursor-pointer" (click)="toggleShowPayment()">
<span class="tw-ml-2 tw-text-primary-600 tw-cursor-pointer" (click)="toggleShowPayment()">
{{ "changePaymentMethod" | i18n }}
</span>
<a></a>
@@ -382,8 +381,8 @@
</p>
</div>
<!-- SM + PM and PM only cost summary -->
<div *ngIf="totalOpened && !isSecretsManagerTrial()" class="row">
<bit-hint class="col-6" *ngIf="selectedInterval == planIntervals.Annually">
<div *ngIf="totalOpened && !isSecretsManagerTrial()" class="tw-flex tw-flex-wrap tw-gap-4">
<bit-hint class="tw-w-1/2" *ngIf="selectedInterval == planIntervals.Annually">
<p class="tw-font-semibold tw-mb-1" *ngIf="organization.useSecretsManager">
{{ "passwordManager" | i18n }}
</p>
@@ -550,7 +549,7 @@
</ng-container>
</p>
</bit-hint>
<bit-hint class="col-6" *ngIf="selectedInterval == planIntervals.Monthly">
<bit-hint class="tw-w-1/2" *ngIf="selectedInterval == planIntervals.Monthly">
<p class="tw-font-semibold tw-mb-1" *ngIf="organization.useSecretsManager">
{{ "passwordManager" | i18n }}
</p>
@@ -706,8 +705,8 @@
</bit-hint>
</div>
<!-- SM + Free PM cost summary -->
<div *ngIf="totalOpened && isSecretsManagerTrial()" class="row">
<bit-hint class="col-6" *ngIf="selectedInterval == planIntervals.Annually">
<div *ngIf="totalOpened && isSecretsManagerTrial()" class="tw-flex tw-flex-wrap tw-gap-4">
<bit-hint class="tw-w-1/2" *ngIf="selectedInterval == planIntervals.Annually">
<!-- secrets manager summary for annual -->
<p class="tw-font-semibold tw-mt-2 tw-mb-0" *ngIf="organization.useSecretsManager">
{{ "secretsManager" | i18n }}
@@ -826,7 +825,7 @@
</span>
</p>
</bit-hint>
<bit-hint class="col-6" *ngIf="selectedInterval == planIntervals.Monthly">
<bit-hint class="tw-w-1/2" *ngIf="selectedInterval == planIntervals.Monthly">
<!-- secrets manager summary for monthly -->
<p class="tw-font-semibold tw-mt-2 tw-mb-0" *ngIf="organization.useSecretsManager">
{{ "secretsManager" | i18n }}
@@ -930,9 +929,9 @@
<!-- discountPercentage to PM Only -->
<div
*ngIf="totalOpened && discountPercentage && !organization.useSecretsManager"
class="row"
class="tw-flex tw-flex-wrap tw-gap-4"
>
<bit-hint class="col-6">
<bit-hint class="tw-w-1/2">
<p
class="tw-mb-0 tw-flex tw-justify-between"
bitTypography="body2"
@@ -949,8 +948,8 @@
</p>
</bit-hint>
</div>
<div *ngIf="totalOpened" class="row tw-mt-4">
<bit-hint class="col-6">
<div *ngIf="totalOpened" class="tw-flex tw-flex-wrap tw-gap-4 tw-mt-4">
<bit-hint class="tw-w-1/2">
<p
class="tw-flex tw-justify-between tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-pt-2 tw-mb-0"
>
@@ -963,8 +962,8 @@
</p>
</bit-hint>
</div>
<div *ngIf="totalOpened" class="row tw-mt-4">
<bit-hint class="col-6">
<div *ngIf="totalOpened" class="tw-flex tw-flex-wrap tw-gap-4 tw-mt-4">
<bit-hint class="tw-w-1/2">
<p
class="tw-flex tw-justify-between tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-pt-2 tw-mb-0"
>

View File

@@ -216,7 +216,7 @@
formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}"
/>
<bit-hint class="tx-text-sm"
<bit-hint class="tw-text-sm"
>{{
"userSeatsAdditionalDesc"
| i18n

View File

@@ -1,7 +1,7 @@
<app-header></app-header>
<ng-container *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin text-muted" title="{{ 'loading' | i18n }}"></i>
<i class="bwi bwi-spinner bwi-spin tw-text-muted" title="{{ 'loading' | i18n }}"></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container>

View File

@@ -42,7 +42,7 @@ export class InitService {
init() {
return async () => {
await this.sdkLoadService.load();
await this.sdkLoadService.loadAndInit();
await this.stateService.init();
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);

View File

@@ -11,7 +11,6 @@ import {
unauthGuardFn,
activeAuthGuard,
} from "@bitwarden/angular/auth/guards";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
import {
AnonLayoutWrapperComponent,
@@ -42,7 +41,6 @@ import {
NewDeviceVerificationComponent,
DeviceVerificationIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LockComponent } from "@bitwarden/key-management-ui";
import {
NewDeviceVerificationNoticePageOneComponent,
@@ -611,11 +609,7 @@ const routes: Routes = [
},
{
path: "device-verification",
canActivate: [
canAccessFeature(FeatureFlag.NewDeviceVerification),
unauthGuardFn(),
activeAuthGuard(),
],
canActivate: [unauthGuardFn(), activeAuthGuard()],
children: [
{
path: "",

View File

@@ -18,7 +18,7 @@ const supported = (() => {
return false;
})();
export class WebSdkLoadService implements SdkLoadService {
export class WebSdkLoadService extends SdkLoadService {
async load(): Promise<void> {
let module: any;
if (supported) {

View File

@@ -66,6 +66,9 @@ describe("BreachReportComponent", () => {
useValue: mock<I18nService>(),
},
],
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
errorOnUnknownElements: false,
errorOnUnknownProperties: false,
}).compileComponents();
});

View File

@@ -84,6 +84,9 @@ describe("ExposedPasswordsReportComponent", () => {
},
],
schemas: [],
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
errorOnUnknownElements: false,
errorOnUnknownProperties: false,
}).compileComponents();
});

View File

@@ -47,14 +47,19 @@
<app-vault-icon [cipher]="r"></app-vault-icon>
</td>
<td bitCell>
<a
bitLink
href="#"
appStopClick
(click)="selectCipher(r)"
title="{{ 'editItemWithName' | i18n: r.name }}"
>{{ r.name }}</a
>
<ng-container *ngIf="!organization || canManageCipher(r); else cantManage">
<a
bitLink
href="#"
appStopClick
(click)="selectCipher(r)"
title="{{ 'editItemWithName' | i18n: r.name }}"
>{{ r.name }}</a
>
</ng-container>
<ng-template #cantManage>
<span>{{ r.name }}</span>
</ng-template>
<ng-container *ngIf="!organization && r.organizationId">
<i
class="bwi bwi-collection"

View File

@@ -82,6 +82,8 @@ describe("InactiveTwoFactorReportComponent", () => {
},
],
schemas: [],
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
errorOnUnknownElements: false,
}).compileComponents();
});

View File

@@ -130,4 +130,15 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
this.services.set(serviceData.domain, serviceData.documentation);
}
}
/**
* Provides a way to determine if someone with permissions to run an organizational report is also able to view/edit ciphers within the results
* Default to true for indivduals running reports on their own vault.
* @param c CipherView
* @returns boolean
*/
protected canManageCipher(c: CipherView): boolean {
// this will only ever be false from the org view;
return true;
}
}

View File

@@ -91,6 +91,9 @@ export class ExposedPasswordsReportComponent
}
canManageCipher(c: CipherView): boolean {
if (c.collectionIds.length === 0) {
return true;
}
return this.manageableCiphers.some((x) => x.id === c.id);
}
}

View File

@@ -13,6 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
@@ -41,6 +42,9 @@ export class InactiveTwoFactorReportComponent
extends BaseInactiveTwoFactorReportComponent
implements OnInit
{
// Contains a list of ciphers, the user running the report, can manage
private manageableCiphers: Cipher[];
constructor(
cipherService: CipherService,
dialogService: DialogService,
@@ -80,6 +84,7 @@ export class InactiveTwoFactorReportComponent
.organizations$(userId)
.pipe(getOrganizationById(params.organizationId)),
);
this.manageableCiphers = await this.cipherService.getAll(userId);
await super.ngOnInit();
});
}
@@ -87,4 +92,11 @@ export class InactiveTwoFactorReportComponent
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
protected canManageCipher(c: CipherView): boolean {
if (c.collectionIds.length === 0) {
return true;
}
return this.manageableCiphers.some((x) => x.id === c.id);
}
}

View File

@@ -89,6 +89,9 @@ export class ReusedPasswordsReportComponent
}
canManageCipher(c: CipherView): boolean {
if (c.collectionIds.length === 0) {
return true;
}
return this.manageableCiphers.some((x) => x.id === c.id);
}
}

View File

@@ -13,6 +13,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherFormConfigService, PasswordRepromptService } from "@bitwarden/vault";
@@ -41,6 +42,9 @@ export class UnsecuredWebsitesReportComponent
extends BaseUnsecuredWebsitesReportComponent
implements OnInit
{
// Contains a list of ciphers, the user running the report, can manage
private manageableCiphers: Cipher[];
constructor(
cipherService: CipherService,
dialogService: DialogService,
@@ -80,6 +84,7 @@ export class UnsecuredWebsitesReportComponent
.organizations$(userId)
.pipe(getOrganizationById(params.organizationId)),
);
this.manageableCiphers = await this.cipherService.getAll(userId);
await super.ngOnInit();
});
}
@@ -87,4 +92,11 @@ export class UnsecuredWebsitesReportComponent
getAllCiphers(): Promise<CipherView[]> {
return this.cipherService.getAllFromApiForOrganization(this.organization.id);
}
protected canManageCipher(c: CipherView): boolean {
if (c.collectionIds.length === 0) {
return true;
}
return this.manageableCiphers.some((x) => x.id === c.id);
}
}

View File

@@ -93,6 +93,9 @@ export class WeakPasswordsReportComponent
}
canManageCipher(c: CipherView): boolean {
if (c.collectionIds.length === 0) {
return true;
}
return this.manageableCiphers.some((x) => x.id === c.id);
}
}

Some files were not shown because too many files have changed in this diff Show More