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:
20
.github/renovate.json5
vendored
20
.github/renovate.json5
vendored
@@ -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",
|
||||
|
||||
33
.github/workflows/build-cli.yml
vendored
33
.github/workflows/build-cli.yml
vendored
@@ -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' }}
|
||||
|
||||
3
.github/workflows/build-desktop.yml
vendored
3
.github/workflows/build-desktop.yml
vendored
@@ -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' }}
|
||||
|
||||
@@ -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 Bitwarden’s 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"
|
||||
},
|
||||
|
||||
@@ -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])(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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 |
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
import "core-js/stable";
|
||||
import "core-js/proposals/explicit-resource-management";
|
||||
import "zone.js";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -85,4 +85,5 @@
|
||||
></app-vault-list-items-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<new-settings-callout></new-settings-callout>
|
||||
</popup-page>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "jest-preset-angular/setup-jest";
|
||||
import "@bitwarden/ui-common/setup-jest";
|
||||
import { addCustomMatchers } from "@bitwarden/common/spec";
|
||||
|
||||
addCustomMatchers();
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
44
apps/cli/scripts/sign-cli.ps1
Normal file
44
apps/cli/scripts/sign-cli.ps1
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "core-js/proposals/explicit-resource-management";
|
||||
|
||||
import { program } from "commander";
|
||||
|
||||
import { OssServeConfigurator } from "./oss-serve-configurator";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
159
apps/desktop/desktop_native/Cargo.lock
generated
159
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
9
apps/desktop/desktop_native/napi/index.d.ts
vendored
9
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "core-js/proposals/explicit-resource-management";
|
||||
|
||||
import { enableProdMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -70,6 +70,8 @@ describe("RecoverTwoFactorComponent", () => {
|
||||
},
|
||||
],
|
||||
imports: [I18nPipe],
|
||||
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
|
||||
errorOnUnknownElements: false,
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(RecoverTwoFactorComponent);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
formControlName="additionalSeats"
|
||||
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
||||
/>
|
||||
<bit-hint class="tx-text-sm"
|
||||
<bit-hint class="tw-text-sm"
|
||||
>{{
|
||||
"userSeatsAdditionalDesc"
|
||||
| i18n
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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$);
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -66,6 +66,9 @@ describe("BreachReportComponent", () => {
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
],
|
||||
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
|
||||
errorOnUnknownElements: false,
|
||||
errorOnUnknownProperties: false,
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
|
||||
@@ -84,6 +84,9 @@ describe("ExposedPasswordsReportComponent", () => {
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
|
||||
errorOnUnknownElements: false,
|
||||
errorOnUnknownProperties: false,
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -82,6 +82,8 @@ describe("InactiveTwoFactorReportComponent", () => {
|
||||
},
|
||||
],
|
||||
schemas: [],
|
||||
// FIXME(PM-18598): Replace unknownElements and unknownProperties with actual imports
|
||||
errorOnUnknownElements: false,
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user