mirror of
https://github.com/bitwarden/browser
synced 2026-02-07 04:03:29 +00:00
Merge branch 'main' into auth/pm-22723/policy-service-updates
This commit is contained in:
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -81,7 +81,9 @@ bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev
|
||||
apps/browser/src/platform @bitwarden/team-platform-dev
|
||||
apps/cli/src/platform @bitwarden/team-platform-dev
|
||||
apps/desktop/macos @bitwarden/team-platform-dev
|
||||
apps/desktop/scripts @bitwarden/team-platform-dev
|
||||
apps/desktop/src/platform @bitwarden/team-platform-dev
|
||||
apps/desktop/resources @bitwarden/team-platform-dev
|
||||
apps/web/src/app/platform @bitwarden/team-platform-dev
|
||||
libs/angular/src/platform @bitwarden/team-platform-dev
|
||||
libs/common/src/platform @bitwarden/team-platform-dev
|
||||
@@ -140,6 +142,7 @@ libs/components @bitwarden/team-ui-foundation
|
||||
libs/ui @bitwarden/team-ui-foundation
|
||||
apps/browser/src/platform/popup/layout @bitwarden/team-ui-foundation
|
||||
apps/browser/src/popup/app-routing.animations.ts @bitwarden/team-ui-foundation
|
||||
apps/browser/src/popup/components/extension-anon-layout-wrapper @bitwarden/team-ui-foundation
|
||||
apps/web/src/app/layouts @bitwarden/team-ui-foundation
|
||||
|
||||
|
||||
|
||||
102
.github/workflows/repository-management.yml
vendored
102
.github/workflows/repository-management.yml
vendored
@@ -36,8 +36,7 @@ on:
|
||||
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
|
||||
permissions: {}
|
||||
jobs:
|
||||
setup:
|
||||
name: Setup
|
||||
@@ -57,51 +56,11 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
cut_branch:
|
||||
name: Cut branch
|
||||
if: ${{ needs.setup.outputs.branch == 'rc' }}
|
||||
needs: setup
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Cut branch
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
git switch --quiet --create $BRANCH_NAME
|
||||
git push --quiet --set-upstream origin $BRANCH_NAME
|
||||
|
||||
|
||||
bump_version:
|
||||
name: Bump Version
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- cut_branch
|
||||
- setup
|
||||
needs: setup
|
||||
outputs:
|
||||
version_browser: ${{ steps.set-final-version-output.outputs.version_browser }}
|
||||
version_cli: ${{ steps.set-final-version-output.outputs.version_cli }}
|
||||
@@ -441,15 +400,13 @@ jobs:
|
||||
- name: Push changes
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
run: git push
|
||||
|
||||
|
||||
cherry_pick:
|
||||
name: Cherry-Pick Commit(s)
|
||||
cut_branch:
|
||||
name: Cut branch
|
||||
if: ${{ needs.setup.outputs.branch == 'rc' }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- bump_version
|
||||
- setup
|
||||
- bump_version
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@30bf6253fa41bdc8d1501d202ad15287582246b4 # v2.0.3
|
||||
@@ -458,43 +415,24 @@ jobs:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
|
||||
- name: Check out main branch
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Configure Git
|
||||
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "Github Actions"
|
||||
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Perform cherry-pick(s)
|
||||
- name: Cut branch
|
||||
env:
|
||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||
run: |
|
||||
# Function for cherry-picking
|
||||
cherry_pick () {
|
||||
local package_path="apps/$1/package.json"
|
||||
local source_branch=$2
|
||||
local destination_branch=$3
|
||||
|
||||
# Get project commit/version from source branch
|
||||
git switch $source_branch
|
||||
SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 $package_path)
|
||||
SOURCE_VERSION=$(cat $package_path | jq -r '.version')
|
||||
|
||||
# Get project commit/version from destination branch
|
||||
git switch $destination_branch
|
||||
DESTINATION_VERSION=$(cat $package_path | jq -r '.version')
|
||||
|
||||
if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then
|
||||
git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT
|
||||
git push -u origin $destination_branch
|
||||
fi
|
||||
}
|
||||
|
||||
# Cherry-pick from 'main' into 'rc'
|
||||
cherry_pick browser main rc
|
||||
cherry_pick cli main rc
|
||||
cherry_pick desktop main rc
|
||||
cherry_pick web main rc
|
||||
git switch --quiet --create $BRANCH_NAME
|
||||
git push --quiet --set-upstream origin $BRANCH_NAME
|
||||
@@ -1922,6 +1922,9 @@
|
||||
"typeSshKey": {
|
||||
"message": "SSH key"
|
||||
},
|
||||
"typeNote": {
|
||||
"message": "Note"
|
||||
},
|
||||
"newItemHeader": {
|
||||
"message": "New $TYPE$",
|
||||
"placeholders": {
|
||||
@@ -5062,6 +5065,9 @@
|
||||
"unlockPinSet": {
|
||||
"message": "Unlock PIN set"
|
||||
},
|
||||
"unlockBiometricSet": {
|
||||
"message": "Unlock biometrics set"
|
||||
},
|
||||
"authenticating": {
|
||||
"message": "Authenticating"
|
||||
},
|
||||
@@ -5403,5 +5409,9 @@
|
||||
},
|
||||
"noPermissionsViewPage": {
|
||||
"message": "You do not have permissions to view this page. Try logging in with a different account."
|
||||
},
|
||||
"wasmNotSupported": {
|
||||
"message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.",
|
||||
"description": "'WebAssembly' is a technical term and should not be translated."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../../../popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
|
||||
import { ExtensionLoginComponentService } from "./extension-login-component.service";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../../../popup/components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
|
||||
@Injectable()
|
||||
export class ExtensionLoginComponentService
|
||||
|
||||
@@ -534,6 +534,11 @@ export class AccountSecurityComponent implements OnInit, OnDestroy {
|
||||
if (!successful) {
|
||||
await this.biometricStateService.setFingerprintValidated(false);
|
||||
}
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("unlockBiometricSet"),
|
||||
});
|
||||
} catch (error) {
|
||||
this.form.controls.biometric.setValue(false);
|
||||
this.validationService.showError(error);
|
||||
|
||||
@@ -88,6 +88,7 @@ export class WebauthnUtils {
|
||||
getClientExtensionResults: () => ({
|
||||
credProps: result.extensions.credProps,
|
||||
}),
|
||||
toJSON: () => Fido2Utils.createResultToJson(result),
|
||||
} as PublicKeyCredential;
|
||||
|
||||
// Modify prototype chains to fix `instanceof` calls.
|
||||
@@ -134,6 +135,7 @@ export class WebauthnUtils {
|
||||
} as AuthenticatorAssertionResponse,
|
||||
getClientExtensionResults: () => ({}),
|
||||
authenticatorAttachment: "platform",
|
||||
toJSON: () => Fido2Utils.getResultToJson(result),
|
||||
} as PublicKeyCredential;
|
||||
|
||||
// Modify prototype chains to fix `instanceof` calls.
|
||||
|
||||
@@ -45,6 +45,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import {
|
||||
CardComponent,
|
||||
CheckboxModule,
|
||||
@@ -58,7 +59,6 @@ import {
|
||||
SelectModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
// FIXME (PM-22628): angular imports are forbidden in background
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -20,7 +16,6 @@ import {
|
||||
import { NativeMessagingBackground } from "../../background/nativeMessaging.background";
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
|
||||
@Injectable()
|
||||
export class BackgroundBrowserBiometricsService extends BiometricsService {
|
||||
constructor(
|
||||
private nativeMessagingBackground: () => NativeMessagingBackground,
|
||||
|
||||
@@ -105,9 +105,11 @@ export class PopupRouterCacheService {
|
||||
* Navigate back in history
|
||||
*/
|
||||
async back() {
|
||||
await this.state.update((prevState) => (prevState ? prevState.slice(0, -1) : []));
|
||||
const history = await this.state.update((prevState) =>
|
||||
prevState ? prevState.slice(0, -1) : [],
|
||||
);
|
||||
|
||||
if (this.hasNavigated) {
|
||||
if (this.hasNavigated && history.length) {
|
||||
this.location.back();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,9 +35,9 @@ if (BrowserApi.isManifestVersion(3)) {
|
||||
console.info("WebAssembly is supported in this environment");
|
||||
loadingPromise = import("./wasm");
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info("WebAssembly is not supported in this environment");
|
||||
loadingPromise = import("./fallback");
|
||||
loadingPromise = new Promise((_, reject) => {
|
||||
reject(new Error("WebAssembly is not supported in this environment"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,9 +51,7 @@ async function importModule(): Promise<GlobalWithWasmInit["initSdk"]> {
|
||||
console.info("WebAssembly is supported in this environment");
|
||||
await import("./wasm");
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info("WebAssembly is not supported in this environment");
|
||||
await import("./fallback");
|
||||
throw new Error("WebAssembly is not supported in this environment");
|
||||
}
|
||||
|
||||
// the wasm and fallback imports mutate globalThis to add the initSdk function
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as sdk from "@bitwarden/sdk-internal";
|
||||
import * as wasm from "@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm.js";
|
||||
|
||||
import { GlobalWithWasmInit } from "./browser-sdk-load.service";
|
||||
|
||||
(globalThis as GlobalWithWasmInit).initSdk = () => {
|
||||
(sdk as any).init(wasm);
|
||||
};
|
||||
@@ -16,10 +16,7 @@ import {
|
||||
unauthGuardFn,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
DevicesIcon,
|
||||
LockIcon,
|
||||
LoginComponent,
|
||||
LoginDecryptionOptionsComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
@@ -41,13 +38,10 @@ import {
|
||||
UserLockIcon,
|
||||
VaultIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component";
|
||||
import {
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
ExtensionAnonLayoutWrapperData,
|
||||
} from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { fido2AuthGuard } from "../auth/popup/guards/fido2-auth.guard";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
@@ -89,6 +83,10 @@ import { TrashComponent } from "../vault/popup/settings/trash.component";
|
||||
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
|
||||
|
||||
import { RouteElevation } from "./app-routing.animations";
|
||||
import {
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
ExtensionAnonLayoutWrapperData,
|
||||
} from "./components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
||||
import { TabsV2Component } from "./tabs-v2.component";
|
||||
|
||||
@@ -504,7 +502,7 @@ const routes: Routes = [
|
||||
path: "lock",
|
||||
canActivate: [lockGuard()],
|
||||
data: {
|
||||
pageIcon: LockIcon,
|
||||
pageIcon: Icons.LockIcon,
|
||||
pageTitle: {
|
||||
key: "yourVaultIsLockedV2",
|
||||
},
|
||||
|
||||
@@ -11,7 +11,17 @@ import {
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||
import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap, map } from "rxjs";
|
||||
import {
|
||||
Subject,
|
||||
takeUntil,
|
||||
firstValueFrom,
|
||||
concatMap,
|
||||
filter,
|
||||
tap,
|
||||
catchError,
|
||||
of,
|
||||
map,
|
||||
} from "rxjs";
|
||||
|
||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||
@@ -23,6 +33,7 @@ import { AnimationControlService } from "@bitwarden/common/platform/abstractions
|
||||
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 { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -48,23 +59,45 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
|
||||
styles: [],
|
||||
animations: [routerTransition],
|
||||
template: `
|
||||
<div [@routerTransition]="getRouteElevation(outlet)">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
<bit-toast-container></bit-toast-container>
|
||||
@if (showSdkWarning | async) {
|
||||
<div class="tw-h-screen tw-flex tw-justify-center tw-items-center tw-p-4">
|
||||
<bit-callout type="danger">
|
||||
{{ "wasmNotSupported" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/help/wasm-not-supported/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{{ "learnMore" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
</div>
|
||||
} @else {
|
||||
<div [@routerTransition]="getRouteElevation(outlet)">
|
||||
<router-outlet #outlet="outlet"></router-outlet>
|
||||
</div>
|
||||
<bit-toast-container></bit-toast-container>
|
||||
}
|
||||
`,
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
private compactModeService = inject(PopupCompactModeService);
|
||||
private sdkService = inject(SdkService);
|
||||
|
||||
private lastActivity: Date;
|
||||
private activeUserId: UserId;
|
||||
private recordActivitySubject = new Subject<void>();
|
||||
private routerAnimations = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// Show a warning if the SDK is not available.
|
||||
protected showSdkWarning = this.sdkService.client$.pipe(
|
||||
map(() => false),
|
||||
catchError(() => of(true)),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private i18nService: I18nService,
|
||||
|
||||
@@ -20,11 +20,12 @@ import {
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
ToastModule,
|
||||
CalloutModule,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { AccountComponent } from "../auth/popup/account-switching/account.component";
|
||||
import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component";
|
||||
import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { SetPasswordComponent } from "../auth/popup/set-password.component";
|
||||
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
|
||||
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
|
||||
@@ -42,6 +43,7 @@ import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popou
|
||||
|
||||
import { AppRoutingModule } from "./app-routing.module";
|
||||
import { AppComponent } from "./app.component";
|
||||
import { ExtensionAnonLayoutWrapperComponent } from "./components/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
|
||||
import { UserVerificationComponent } from "./components/user-verification.component";
|
||||
import { ServicesModule } from "./services/services.module";
|
||||
import { TabsV2Component } from "./tabs-v2.component";
|
||||
@@ -87,6 +89,8 @@ import "../platform/popup/locales";
|
||||
CurrentAccountComponent,
|
||||
FormFieldModule,
|
||||
ExtensionAnonLayoutWrapperComponent,
|
||||
CalloutModule,
|
||||
LinkModule,
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Observable, Subject } from "rxjs";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
DefaultAnonLayoutWrapperDataService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { ExtensionAnonLayoutWrapperData } from "./extension-anon-layout-wrapper.component";
|
||||
|
||||
@@ -5,22 +5,23 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
|
||||
import { Subject, filter, switchMap, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
Icon,
|
||||
Icons,
|
||||
IconModule,
|
||||
Translation,
|
||||
AnonLayoutComponent,
|
||||
AnonLayoutWrapperData,
|
||||
AnonLayoutWrapperDataService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Icon, IconModule, Translation } from "@bitwarden/components";
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component";
|
||||
import { AccountSwitcherService } from "../../../auth/popup/account-switching/services/account-switcher.service";
|
||||
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 { CurrentAccountComponent } from "../account-switching/current-account.component";
|
||||
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
|
||||
|
||||
import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon";
|
||||
|
||||
export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData {
|
||||
showAcctSwitcher?: boolean;
|
||||
@@ -61,7 +62,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
|
||||
protected hideFooter: boolean;
|
||||
|
||||
protected theme: string;
|
||||
protected logo = ExtensionBitwardenLogo;
|
||||
protected logo = Icons.ExtensionBitwardenLogo;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AnonLayoutWrapperDataService, LockIcon } from "@bitwarden/auth/angular";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
@@ -23,13 +22,15 @@ import {
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ButtonModule, I18nMockService } from "@bitwarden/components";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
ButtonModule,
|
||||
Icons,
|
||||
I18nMockService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon";
|
||||
import { AccountSwitcherService } from "../../../auth/popup/account-switching/services/account-switcher.service";
|
||||
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";
|
||||
import { AccountSwitcherService } from "../account-switching/services/account-switcher.service";
|
||||
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "./extension-anon-layout-wrapper-data.service";
|
||||
import {
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
} from "./extension-anon-layout-wrapper.component";
|
||||
|
||||
export default {
|
||||
title: "Auth/Extension Anon Layout Wrapper",
|
||||
title: "Browser/Extension Anon Layout Wrapper",
|
||||
component: ExtensionAnonLayoutWrapperComponent,
|
||||
} as Meta;
|
||||
|
||||
@@ -142,6 +143,8 @@ const decorators = (options: {
|
||||
switchAccounts: "Switch accounts",
|
||||
back: "Back",
|
||||
activeAccount: "Active account",
|
||||
appLogoLabel: "app logo label",
|
||||
bitwardenAccount: "Bitwarden Account",
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -241,7 +244,7 @@ const initialData: ExtensionAnonLayoutWrapperData = {
|
||||
pageSubtitle: {
|
||||
key: "finishCreatingYourAccountBySettingAPassword",
|
||||
},
|
||||
pageIcon: LockIcon,
|
||||
pageIcon: Icons.LockIcon,
|
||||
showAcctSwitcher: true,
|
||||
showBackButton: true,
|
||||
showLogo: true,
|
||||
@@ -255,7 +258,7 @@ const changedData: ExtensionAnonLayoutWrapperData = {
|
||||
pageSubtitle: {
|
||||
key: "checkYourEmail",
|
||||
},
|
||||
pageIcon: RegistrationCheckEmailIcon,
|
||||
pageIcon: Icons.RegistrationCheckEmailIcon,
|
||||
showAcctSwitcher: false,
|
||||
showBackButton: false,
|
||||
showLogo: false,
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
LoginComponentService,
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthEmailComponentService,
|
||||
@@ -121,7 +120,12 @@ import {
|
||||
} from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { CompactModeService, DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AnonLayoutWrapperDataService,
|
||||
CompactModeService,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
BiometricsService,
|
||||
@@ -138,7 +142,6 @@ import {
|
||||
|
||||
import { AccountSwitcherService } from "../../auth/popup/account-switching/services/account-switcher.service";
|
||||
import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service";
|
||||
import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service";
|
||||
import { ExtensionLogoutService } from "../../auth/popup/logout/extension-logout.service";
|
||||
@@ -181,6 +184,7 @@ import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-u
|
||||
import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service";
|
||||
import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service";
|
||||
import { VaultFilterService } from "../../vault/services/vault-filter.service";
|
||||
import { ExtensionAnonLayoutWrapperDataService } from "../components/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service";
|
||||
|
||||
import { DebounceNavigationService } from "./debounce-navigation.service";
|
||||
import { InitService } from "./init.service";
|
||||
|
||||
@@ -12,8 +12,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import {
|
||||
RestrictedCipherType,
|
||||
RestrictedItemTypesService,
|
||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
|
||||
@@ -8,9 +8,10 @@ import { map, Observable } from "rxjs";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CipherMenuItem, CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||
import { AddEditFolderDialogComponent, RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
import { AddEditFolderDialogComponent } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
|
||||
@@ -20,7 +20,10 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
import {
|
||||
RestrictedCipherType,
|
||||
RestrictedItemTypesService,
|
||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
|
||||
import {
|
||||
CachedFilterState,
|
||||
|
||||
@@ -39,9 +39,12 @@ import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import {
|
||||
isCipherViewRestricted,
|
||||
RestrictedItemTypesService,
|
||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import { ChipSelectOption } from "@bitwarden/components";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
const FILTER_VISIBILITY_KEY = new KeyDefinition<boolean>(VAULT_SETTINGS_DISK, "filterVisibility", {
|
||||
deserializer: (obj) => obj,
|
||||
@@ -227,18 +230,8 @@ export class VaultPopupListFiltersService {
|
||||
}
|
||||
|
||||
// Check if cipher type is restricted (with organization exemptions)
|
||||
if (restrictions && restrictions.length > 0) {
|
||||
const isRestricted = restrictions.some(
|
||||
(restrictedType) =>
|
||||
restrictedType.cipherType === cipher.type &&
|
||||
(cipher.organizationId
|
||||
? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
|
||||
: restrictedType.allowViewOrgIds.length === 0),
|
||||
);
|
||||
|
||||
if (isRestricted) {
|
||||
return false;
|
||||
}
|
||||
if (isCipherViewRestricted(cipher, restrictions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.cipherType !== null && cipher.type !== filters.cipherType) {
|
||||
|
||||
@@ -89,7 +89,7 @@ async function run(context) {
|
||||
} else {
|
||||
// For non-Appstore builds, we don't need the inherit binary as they are not sandboxed,
|
||||
// but we sign and include it anyway for consistency. It should be removed once DDG supports the proxy directly.
|
||||
const entitlementsName = "entitlements.mac.plist";
|
||||
const entitlementsName = "entitlements.mac.inherit.plist";
|
||||
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
|
||||
child_process.execSync(
|
||||
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,
|
||||
|
||||
@@ -16,11 +16,8 @@ import {
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
LoginComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LockIcon,
|
||||
LoginViaAuthRequestComponent,
|
||||
PasswordHintComponent,
|
||||
RegistrationFinishComponent,
|
||||
@@ -42,6 +39,7 @@ import {
|
||||
DeviceVerificationIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
@@ -292,7 +290,7 @@ const routes: Routes = [
|
||||
path: "lock",
|
||||
canActivate: [lockGuard()],
|
||||
data: {
|
||||
pageIcon: LockIcon,
|
||||
pageIcon: Icons.LockIcon,
|
||||
pageTitle: {
|
||||
key: "yourVaultIsLockedV2",
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||
import { CalloutModule, DialogModule } from "@bitwarden/components";
|
||||
import { AssignCollectionsComponent } from "@bitwarden/vault";
|
||||
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { LoginModule } from "../auth/login/login.module";
|
||||
@@ -55,6 +56,7 @@ import { SharedModule } from "./shared/shared.module";
|
||||
DeleteAccountComponent,
|
||||
UserVerificationComponent,
|
||||
NavComponent,
|
||||
AssignCollectionsComponent,
|
||||
VaultV2Component,
|
||||
],
|
||||
declarations: [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -6,10 +7,10 @@ import { BiometricsService } from "@bitwarden/key-management";
|
||||
* specifically for the main process.
|
||||
*/
|
||||
export abstract class DesktopBiometricsService extends BiometricsService {
|
||||
abstract setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise<void>;
|
||||
abstract setBiometricProtectedUnlockKeyForUser(
|
||||
userId: UserId,
|
||||
value: SymmetricCryptoKey,
|
||||
): Promise<void>;
|
||||
abstract deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void>;
|
||||
|
||||
abstract setupBiometrics(): Promise<void>;
|
||||
|
||||
abstract setClientKeyHalfForUser(userId: UserId, value: string | null): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
@@ -37,17 +38,12 @@ export class MainBiometricsIPCListener {
|
||||
}
|
||||
return await this.biometricService.setBiometricProtectedUnlockKeyForUser(
|
||||
message.userId as UserId,
|
||||
message.key,
|
||||
SymmetricCryptoKey.fromString(message.key),
|
||||
);
|
||||
case BiometricAction.RemoveKeyForUser:
|
||||
return await this.biometricService.deleteBiometricUnlockKeyForUser(
|
||||
message.userId as UserId,
|
||||
);
|
||||
case BiometricAction.SetClientKeyHalf:
|
||||
return await this.biometricService.setClientKeyHalfForUser(
|
||||
message.userId as UserId,
|
||||
message.key,
|
||||
);
|
||||
case BiometricAction.Setup:
|
||||
return await this.biometricService.setupBiometrics();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
BiometricsService,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
import { MainCryptoFunctionService } from "../../platform/main/main-crypto-function.service";
|
||||
|
||||
import { MainBiometricsService } from "./main-biometrics.service";
|
||||
import OsBiometricsServiceLinux from "./os-biometrics-linux.service";
|
||||
@@ -27,21 +28,25 @@ jest.mock("@bitwarden/desktop-napi", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const unlockKey = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
|
||||
describe("MainBiometricsService", function () {
|
||||
const i18nService = mock<I18nService>();
|
||||
const windowMain = mock<WindowMain>();
|
||||
const logService = mock<LogService>();
|
||||
const messagingService = mock<MessagingService>();
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
const cryptoFunctionService = mock<MainCryptoFunctionService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
||||
it("Should call the platformspecific methods", async () => {
|
||||
const sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
const mockService = mock<OsBiometricService>();
|
||||
@@ -57,9 +62,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
"win32",
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
const internalService = (sut as any).osBiometricsService;
|
||||
@@ -72,9 +78,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
"darwin",
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const internalService = (sut as any).osBiometricsService;
|
||||
expect(internalService).not.toBeNull();
|
||||
@@ -86,9 +93,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
"linux",
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
const internalService = (sut as any).osBiometricsService;
|
||||
@@ -106,9 +114,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
innerService = mock();
|
||||
@@ -131,9 +140,9 @@ describe("MainBiometricsService", function () {
|
||||
];
|
||||
|
||||
for (const [supportsBiometric, needsSetup, canAutoSetup, expected] of testCases) {
|
||||
innerService.osSupportsBiometric.mockResolvedValue(supportsBiometric as boolean);
|
||||
innerService.osBiometricsNeedsSetup.mockResolvedValue(needsSetup as boolean);
|
||||
innerService.osBiometricsCanAutoSetup.mockResolvedValue(canAutoSetup as boolean);
|
||||
innerService.supportsBiometrics.mockResolvedValue(supportsBiometric as boolean);
|
||||
innerService.needsSetup.mockResolvedValue(needsSetup as boolean);
|
||||
innerService.canAutoSetup.mockResolvedValue(canAutoSetup as boolean);
|
||||
|
||||
const actual = await sut.getBiometricsStatus();
|
||||
expect(actual).toBe(expected);
|
||||
@@ -175,12 +184,23 @@ describe("MainBiometricsService", function () {
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(
|
||||
requirePasswordOnStart as boolean,
|
||||
);
|
||||
(sut as any).clientKeyHalves = new Map();
|
||||
const userId = "test" as UserId;
|
||||
if (hasKeyHalf) {
|
||||
(sut as any).clientKeyHalves.set(userId, "test");
|
||||
if (!requirePasswordOnStart) {
|
||||
(sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest
|
||||
.fn()
|
||||
.mockResolvedValue(BiometricsStatus.Available);
|
||||
} else {
|
||||
if (hasKeyHalf) {
|
||||
(sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest
|
||||
.fn()
|
||||
.mockResolvedValue(BiometricsStatus.Available);
|
||||
} else {
|
||||
(sut as any).osBiometricsService.getBiometricsFirstUnlockStatusForUser = jest
|
||||
.fn()
|
||||
.mockResolvedValue(BiometricsStatus.UnlockNeeded);
|
||||
}
|
||||
}
|
||||
|
||||
const userId = "test" as UserId;
|
||||
const actual = await sut.getBiometricsStatusForUser(userId);
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
@@ -193,50 +213,17 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
|
||||
await sut.setupBiometrics();
|
||||
|
||||
expect(osBiometricsService.osBiometricsSetup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setClientKeyHalfForUser", () => {
|
||||
let sut: MainBiometricsService;
|
||||
|
||||
beforeEach(() => {
|
||||
sut = new MainBiometricsService(
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should set the client key half for the user", async () => {
|
||||
const userId = "test" as UserId;
|
||||
const keyHalf = "testKeyHalf";
|
||||
|
||||
await sut.setClientKeyHalfForUser(userId, keyHalf);
|
||||
|
||||
expect((sut as any).clientKeyHalves.has(userId)).toBe(true);
|
||||
expect((sut as any).clientKeyHalves.get(userId)).toBe(keyHalf);
|
||||
});
|
||||
|
||||
it("should reset the client key half for the user", async () => {
|
||||
const userId = "test" as UserId;
|
||||
|
||||
await sut.setClientKeyHalfForUser(userId, null);
|
||||
|
||||
expect((sut as any).clientKeyHalves.has(userId)).toBe(true);
|
||||
expect((sut as any).clientKeyHalves.get(userId)).toBe(null);
|
||||
expect(osBiometricsService.runSetup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -246,9 +233,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
@@ -268,9 +256,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
@@ -278,34 +267,24 @@ describe("MainBiometricsService", function () {
|
||||
|
||||
it("should return null if no biometric key is returned ", async () => {
|
||||
const userId = "test" as UserId;
|
||||
(sut as any).clientKeyHalves.set(userId, "testKeyHalf");
|
||||
|
||||
osBiometricsService.getBiometricKey.mockResolvedValue(null);
|
||||
const userKey = await sut.unlockWithBiometricsForUser(userId);
|
||||
|
||||
expect(userKey).toBeNull();
|
||||
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
"testKeyHalf",
|
||||
);
|
||||
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
it("should return the biometric key if a valid key is returned", async () => {
|
||||
const userId = "test" as UserId;
|
||||
(sut as any).clientKeyHalves.set(userId, "testKeyHalf");
|
||||
const biometricKey = Utils.fromBufferToB64(new Uint8Array(64));
|
||||
const biometricKey = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
osBiometricsService.getBiometricKey.mockResolvedValue(biometricKey);
|
||||
|
||||
const userKey = await sut.unlockWithBiometricsForUser(userId);
|
||||
|
||||
expect(userKey).not.toBeNull();
|
||||
expect(userKey!.keyB64).toBe(biometricKey);
|
||||
expect(userKey!.keyB64).toBe(biometricKey.toBase64());
|
||||
expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64);
|
||||
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
"testKeyHalf",
|
||||
);
|
||||
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,37 +297,21 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
});
|
||||
|
||||
it("should throw an error if no client key half is provided", async () => {
|
||||
const userId = "test" as UserId;
|
||||
const unlockKey = "testUnlockKey";
|
||||
|
||||
await expect(sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey)).rejects.toThrow(
|
||||
"No client key half provided for user",
|
||||
);
|
||||
});
|
||||
|
||||
it("should call the platform specific setBiometricKey method", async () => {
|
||||
const userId = "test" as UserId;
|
||||
const unlockKey = "testUnlockKey";
|
||||
|
||||
(sut as any).clientKeyHalves.set(userId, "testKeyHalf");
|
||||
|
||||
await sut.setBiometricProtectedUnlockKeyForUser(userId, unlockKey);
|
||||
|
||||
expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
unlockKey,
|
||||
"testKeyHalf",
|
||||
);
|
||||
expect(osBiometricsService.setBiometricKey).toHaveBeenCalledWith(userId, unlockKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -358,9 +321,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
const osBiometricsService = mock<OsBiometricService>();
|
||||
(sut as any).osBiometricsService = osBiometricsService;
|
||||
@@ -369,10 +333,7 @@ describe("MainBiometricsService", function () {
|
||||
|
||||
await sut.deleteBiometricUnlockKeyForUser(userId);
|
||||
|
||||
expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
);
|
||||
expect(osBiometricsService.deleteBiometricKey).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -384,9 +345,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -413,9 +375,10 @@ describe("MainBiometricsService", function () {
|
||||
i18nService,
|
||||
windowMain,
|
||||
logService,
|
||||
messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
|
||||
const shouldAutoPrompt = await sut.getShouldAutopromptNow();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
@@ -13,16 +14,16 @@ import { OsBiometricService } from "./os-biometrics.service";
|
||||
|
||||
export class MainBiometricsService extends DesktopBiometricsService {
|
||||
private osBiometricsService: OsBiometricService;
|
||||
private clientKeyHalves = new Map<string, string | null>();
|
||||
private shouldAutoPrompt = true;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private logService: LogService,
|
||||
private messagingService: MessagingService,
|
||||
private platform: NodeJS.Platform,
|
||||
platform: NodeJS.Platform,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {
|
||||
super();
|
||||
if (platform === "win32") {
|
||||
@@ -32,6 +33,9 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
this.i18nService,
|
||||
this.windowMain,
|
||||
this.logService,
|
||||
this.biometricStateService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
);
|
||||
} else if (platform === "darwin") {
|
||||
// eslint-disable-next-line
|
||||
@@ -40,7 +44,11 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
} else if (platform === "linux") {
|
||||
// eslint-disable-next-line
|
||||
const OsBiometricsServiceLinux = require("./os-biometrics-linux.service").default;
|
||||
this.osBiometricsService = new OsBiometricsServiceLinux(this.i18nService, this.windowMain);
|
||||
this.osBiometricsService = new OsBiometricsServiceLinux(
|
||||
this.biometricStateService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unsupported platform");
|
||||
}
|
||||
@@ -55,11 +63,11 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
* @returns the status of the biometrics of the platform
|
||||
*/
|
||||
async getBiometricsStatus(): Promise<BiometricsStatus> {
|
||||
if (!(await this.osBiometricsService.osSupportsBiometric())) {
|
||||
if (!(await this.osBiometricsService.supportsBiometrics())) {
|
||||
return BiometricsStatus.HardwareUnavailable;
|
||||
} else {
|
||||
if (await this.osBiometricsService.osBiometricsNeedsSetup()) {
|
||||
if (await this.osBiometricsService.osBiometricsCanAutoSetup()) {
|
||||
if (await this.osBiometricsService.needsSetup()) {
|
||||
if (await this.osBiometricsService.canAutoSetup()) {
|
||||
return BiometricsStatus.AutoSetupNeeded;
|
||||
} else {
|
||||
return BiometricsStatus.ManualSetupNeeded;
|
||||
@@ -80,20 +88,12 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
if (!(await this.biometricStateService.getBiometricUnlockEnabled(userId))) {
|
||||
return BiometricsStatus.NotEnabledLocally;
|
||||
}
|
||||
|
||||
const platformStatus = await this.getBiometricsStatus();
|
||||
if (!(platformStatus === BiometricsStatus.Available)) {
|
||||
return platformStatus;
|
||||
}
|
||||
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
const clientKeyHalfB64 = this.clientKeyHalves.get(userId);
|
||||
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
||||
if (!clientKeyHalfSatisfied) {
|
||||
return BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
|
||||
return BiometricsStatus.Available;
|
||||
return await this.osBiometricsService.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
@@ -101,11 +101,7 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
}
|
||||
|
||||
async setupBiometrics(): Promise<void> {
|
||||
return await this.osBiometricsService.osBiometricsSetup();
|
||||
}
|
||||
|
||||
async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise<void> {
|
||||
this.clientKeyHalves.set(userId, value);
|
||||
return await this.osBiometricsService.runSetup();
|
||||
}
|
||||
|
||||
async authenticateWithBiometrics(): Promise<boolean> {
|
||||
@@ -113,43 +109,23 @@ export class MainBiometricsService extends DesktopBiometricsService {
|
||||
}
|
||||
|
||||
async unlockWithBiometricsForUser(userId: UserId): Promise<UserKey | null> {
|
||||
const biometricKey = await this.osBiometricsService.getBiometricKey(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
this.clientKeyHalves.get(userId) ?? undefined,
|
||||
);
|
||||
if (biometricKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SymmetricCryptoKey.fromString(biometricKey) as UserKey;
|
||||
return (await this.osBiometricsService.getBiometricKey(userId)) as UserKey;
|
||||
}
|
||||
|
||||
async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise<void> {
|
||||
const service = "Bitwarden_biometric";
|
||||
const storageKey = `${userId}_user_biometric`;
|
||||
if (!this.clientKeyHalves.has(userId)) {
|
||||
throw new Error("No client key half provided for user");
|
||||
}
|
||||
|
||||
return await this.osBiometricsService.setBiometricKey(
|
||||
service,
|
||||
storageKey,
|
||||
value,
|
||||
this.clientKeyHalves.get(userId) ?? undefined,
|
||||
);
|
||||
async setBiometricProtectedUnlockKeyForUser(
|
||||
userId: UserId,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<void> {
|
||||
return await this.osBiometricsService.setBiometricKey(userId, key);
|
||||
}
|
||||
|
||||
async deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void> {
|
||||
return await this.osBiometricsService.deleteBiometricKey(
|
||||
"Bitwarden_biometric",
|
||||
`${userId}_user_biometric`,
|
||||
);
|
||||
return await this.osBiometricsService.deleteBiometricKey(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to auto-prompt the user for biometric unlock; this can be used to prevent auto-prompting being initiated by a process reload.
|
||||
* Reasons for enabling auto prompt include: Starting the app, un-minimizing the app, manually account switching
|
||||
* Reasons for enabling auto-prompt include: Starting the app, un-minimizing the app, manually account switching
|
||||
* @param value Whether to auto-prompt the user for biometric unlock
|
||||
*/
|
||||
async setShouldAutopromptNow(value: boolean): Promise<void> {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { spawn } from "child_process";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { biometrics, passwords } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
import { isFlatpak, isLinux, isSnapStore } from "../../utils";
|
||||
|
||||
import { OsBiometricService } from "./os-biometrics.service";
|
||||
@@ -28,59 +32,62 @@ const polkitPolicy = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
const policyFileName = "com.bitwarden.Bitwarden.policy";
|
||||
const policyPath = "/usr/share/polkit-1/actions/";
|
||||
|
||||
const SERVICE = "Bitwarden_biometric";
|
||||
function getLookupKeyForUser(userId: UserId): string {
|
||||
return `${userId}_user_biometric`;
|
||||
}
|
||||
|
||||
export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
constructor(
|
||||
private i18nservice: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {}
|
||||
private _iv: string | null = null;
|
||||
// Use getKeyMaterial helper instead of direct access
|
||||
private _osKeyHalf: string | null = null;
|
||||
private clientKeyHalves = new Map<UserId, Uint8Array | null>();
|
||||
|
||||
async setBiometricKey(
|
||||
service: string,
|
||||
key: string,
|
||||
value: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<void> {
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
const clientKeyPartB64 = Utils.fromBufferToB64(
|
||||
await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key),
|
||||
);
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
await biometrics.setBiometricSecret(
|
||||
service,
|
||||
key,
|
||||
value,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
key.toBase64(),
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
}
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
await passwords.deletePassword(service, key);
|
||||
async deleteBiometricKey(userId: UserId): Promise<void> {
|
||||
await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId));
|
||||
}
|
||||
|
||||
async getBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<string | null> {
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
const success = await this.authenticateBiometric();
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Biometric authentication failed");
|
||||
}
|
||||
|
||||
const value = await passwords.getPassword(service, storageKey);
|
||||
const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
|
||||
|
||||
if (value == null || value == "") {
|
||||
return null;
|
||||
} else {
|
||||
const clientKeyHalf = this.clientKeyHalves.get(userId);
|
||||
const clientKeyPartB64 = Utils.fromBufferToB64(clientKeyHalf);
|
||||
const encValue = new EncString(value);
|
||||
this.setIv(encValue.iv);
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
const storedValue = await biometrics.getBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
storageDetails.key_material,
|
||||
);
|
||||
return storedValue;
|
||||
return SymmetricCryptoKey.fromString(storedValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +96,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return await biometrics.prompt(hwnd, "");
|
||||
}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
// We assume all linux distros have some polkit implementation
|
||||
// that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup.
|
||||
// Snap does not have access at the moment to polkit
|
||||
@@ -99,7 +106,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return await passwords.isAvailable();
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup(): Promise<boolean> {
|
||||
async needsSetup(): Promise<boolean> {
|
||||
if (isSnapStore()) {
|
||||
return false;
|
||||
}
|
||||
@@ -108,7 +115,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return !(await biometrics.available());
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
// We cannot auto setup on snap or flatpak since the filesystem is sandboxed.
|
||||
// The user needs to manually set up the polkit policy outside of the sandbox
|
||||
// since we allow access to polkit via dbus for the sandboxed clients, the authentication works from
|
||||
@@ -116,7 +123,7 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
return isLinux() && !isSnapStore() && !isFlatpak();
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {
|
||||
async runSetup(): Promise<void> {
|
||||
const process = spawn("pkexec", [
|
||||
"bash",
|
||||
"-c",
|
||||
@@ -165,4 +172,46 @@ export default class OsBiometricsServiceLinux implements OsBiometricService {
|
||||
ivB64: this._iv,
|
||||
};
|
||||
}
|
||||
|
||||
private async getOrCreateBiometricEncryptionClientKeyHalf(
|
||||
userId: UserId,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array | null> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.clientKeyHalves.has(userId)) {
|
||||
return this.clientKeyHalves.get(userId) || null;
|
||||
}
|
||||
|
||||
// Retrieve existing key half if it exists
|
||||
let clientKeyHalf: Uint8Array | null = null;
|
||||
const encryptedClientKeyHalf =
|
||||
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
if (encryptedClientKeyHalf != null) {
|
||||
clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key);
|
||||
}
|
||||
if (clientKeyHalf == null) {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
const encKey = await this.encryptService.encryptBytes(keyBytes, key);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
this.clientKeyHalves.set(userId, clientKeyHalf);
|
||||
|
||||
return clientKeyHalf;
|
||||
}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
const clientKeyHalfB64 = this.clientKeyHalves.get(userId);
|
||||
const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64;
|
||||
if (!clientKeyHalfSatisfied) {
|
||||
return BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
return BiometricsStatus.Available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { systemPreferences } from "electron";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { passwords } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
import { OsBiometricService } from "./os-biometrics.service";
|
||||
|
||||
const SERVICE = "Bitwarden_biometric";
|
||||
function getLookupKeyForUser(userId: UserId): string {
|
||||
return `${userId}_user_biometric`;
|
||||
}
|
||||
|
||||
export default class OsBiometricsServiceMac implements OsBiometricService {
|
||||
constructor(private i18nservice: I18nService) {}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
return systemPreferences.canPromptTouchID();
|
||||
}
|
||||
|
||||
@@ -21,44 +29,52 @@ export default class OsBiometricsServiceMac implements OsBiometricService {
|
||||
}
|
||||
}
|
||||
|
||||
async getBiometricKey(service: string, key: string): Promise<string | null> {
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
const success = await this.authenticateBiometric();
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Biometric authentication failed");
|
||||
}
|
||||
const keyB64 = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
|
||||
if (keyB64 == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await passwords.getPassword(service, key);
|
||||
return SymmetricCryptoKey.fromString(keyB64);
|
||||
}
|
||||
|
||||
async setBiometricKey(service: string, key: string, value: string): Promise<void> {
|
||||
if (await this.valueUpToDate(service, key, value)) {
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
if (await this.valueUpToDate(userId, key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await passwords.setPassword(service, key, value);
|
||||
return await passwords.setPassword(SERVICE, getLookupKeyForUser(userId), key.toBase64());
|
||||
}
|
||||
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
return await passwords.deletePassword(service, key);
|
||||
async deleteBiometricKey(user: UserId): Promise<void> {
|
||||
return await passwords.deletePassword(SERVICE, getLookupKeyForUser(user));
|
||||
}
|
||||
|
||||
private async valueUpToDate(service: string, key: string, value: string): Promise<boolean> {
|
||||
private async valueUpToDate(user: UserId, key: SymmetricCryptoKey): Promise<boolean> {
|
||||
try {
|
||||
const existing = await passwords.getPassword(service, key);
|
||||
return existing === value;
|
||||
const existing = await passwords.getPassword(SERVICE, getLookupKeyForUser(user));
|
||||
return existing === key.toBase64();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup() {
|
||||
async needsSetup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {}
|
||||
async runSetup(): Promise<void> {}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
return BiometricsStatus.Available;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import OsBiometricsServiceWindows from "./os-biometrics-windows.service";
|
||||
|
||||
jest.mock("@bitwarden/desktop-napi", () => ({
|
||||
biometrics: {
|
||||
available: jest.fn(),
|
||||
setBiometricSecret: jest.fn(),
|
||||
getBiometricSecret: jest.fn(),
|
||||
deriveKeyMaterial: jest.fn(),
|
||||
prompt: jest.fn(),
|
||||
},
|
||||
passwords: {
|
||||
getPassword: jest.fn(),
|
||||
deletePassword: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("OsBiometricsServiceWindows", () => {
|
||||
let service: OsBiometricsServiceWindows;
|
||||
let biometricStateService: BiometricStateService;
|
||||
|
||||
beforeEach(() => {
|
||||
const i18nService = mock<I18nService>();
|
||||
const logService = mock<LogService>();
|
||||
biometricStateService = mock<BiometricStateService>();
|
||||
const encryptionService = mock<EncryptService>();
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
service = new OsBiometricsServiceWindows(
|
||||
i18nService,
|
||||
null,
|
||||
logService,
|
||||
biometricStateService,
|
||||
encryptionService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("getBiometricsFirstUnlockStatusForUser", () => {
|
||||
const userId = "test-user-id" as UserId;
|
||||
it("should return Available when requirePasswordOnRestart is false", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false);
|
||||
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
expect(result).toBe(BiometricsStatus.Available);
|
||||
});
|
||||
it("should return Available when requirePasswordOnRestart is true and client key half is set", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
(service as any).clientKeyHalves = new Map<string, Uint8Array>();
|
||||
(service as any).clientKeyHalves.set(userId, new Uint8Array([1, 2, 3, 4]));
|
||||
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
expect(result).toBe(BiometricsStatus.Available);
|
||||
});
|
||||
it("should return UnlockNeeded when requirePasswordOnRestart is true and client key half is not set", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
(service as any).clientKeyHalves = new Map<string, Uint8Array>();
|
||||
const result = await service.getBiometricsFirstUnlockStatusForUser(userId);
|
||||
expect(result).toBe(BiometricsStatus.UnlockNeeded);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrCreateBiometricEncryptionClientKeyHalf", () => {
|
||||
const userId = "test-user-id" as UserId;
|
||||
const key = new SymmetricCryptoKey(new Uint8Array(64));
|
||||
let encryptionService: EncryptService;
|
||||
let cryptoFunctionService: CryptoFunctionService;
|
||||
|
||||
beforeEach(() => {
|
||||
encryptionService = mock<EncryptService>();
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
service = new OsBiometricsServiceWindows(
|
||||
mock<I18nService>(),
|
||||
null,
|
||||
mock<LogService>(),
|
||||
biometricStateService,
|
||||
encryptionService,
|
||||
cryptoFunctionService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null if getRequirePasswordOnRestart is false", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(false);
|
||||
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return cached key half if already present", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
const cachedKeyHalf = new Uint8Array([10, 20, 30]);
|
||||
(service as any).clientKeyHalves.set(userId.toString(), cachedKeyHalf);
|
||||
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
expect(result).toBe(cachedKeyHalf);
|
||||
});
|
||||
|
||||
it("should decrypt and return existing encrypted client key half", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
biometricStateService.getEncryptedClientKeyHalf = jest
|
||||
.fn()
|
||||
.mockResolvedValue(new Uint8Array([1, 2, 3]));
|
||||
const decrypted = new Uint8Array([4, 5, 6]);
|
||||
encryptionService.decryptBytes = jest.fn().mockResolvedValue(decrypted);
|
||||
|
||||
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
|
||||
expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(userId);
|
||||
expect(encryptionService.decryptBytes).toHaveBeenCalledWith(new Uint8Array([1, 2, 3]), key);
|
||||
expect(result).toEqual(decrypted);
|
||||
expect((service as any).clientKeyHalves.get(userId.toString())).toEqual(decrypted);
|
||||
});
|
||||
|
||||
it("should generate, encrypt, store, and cache a new key half if none exists", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart = jest.fn().mockResolvedValue(true);
|
||||
biometricStateService.getEncryptedClientKeyHalf = jest.fn().mockResolvedValue(null);
|
||||
const randomBytes = new Uint8Array([7, 8, 9]);
|
||||
cryptoFunctionService.randomBytes = jest.fn().mockResolvedValue(randomBytes);
|
||||
const encrypted = new Uint8Array([10, 11, 12]);
|
||||
encryptionService.encryptBytes = jest.fn().mockResolvedValue(encrypted);
|
||||
biometricStateService.setEncryptedClientKeyHalf = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const result = await service.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
|
||||
expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32);
|
||||
expect(encryptionService.encryptBytes).toHaveBeenCalledWith(randomBytes, key);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith(
|
||||
encrypted,
|
||||
userId,
|
||||
);
|
||||
expect(result).toBeNull();
|
||||
expect((service as any).clientKeyHalves.get(userId.toString())).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,14 @@
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { biometrics, passwords } from "@bitwarden/desktop-napi";
|
||||
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
|
||||
@@ -13,87 +17,107 @@ import { OsBiometricService } from "./os-biometrics.service";
|
||||
const KEY_WITNESS_SUFFIX = "_witness";
|
||||
const WITNESS_VALUE = "known key";
|
||||
|
||||
const SERVICE = "Bitwarden_biometric";
|
||||
function getLookupKeyForUser(userId: UserId): string {
|
||||
return `${userId}_user_biometric`;
|
||||
}
|
||||
|
||||
export default class OsBiometricsServiceWindows implements OsBiometricService {
|
||||
// Use set helper method instead of direct access
|
||||
private _iv: string | null = null;
|
||||
// Use getKeyMaterial helper instead of direct access
|
||||
private _osKeyHalf: string | null = null;
|
||||
private clientKeyHalves = new Map<UserId, Uint8Array>();
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private windowMain: WindowMain,
|
||||
private logService: LogService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private encryptService: EncryptService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
) {}
|
||||
|
||||
async osSupportsBiometric(): Promise<boolean> {
|
||||
async supportsBiometrics(): Promise<boolean> {
|
||||
return await biometrics.available();
|
||||
}
|
||||
|
||||
async getBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
clientKeyHalfB64: string,
|
||||
): Promise<string | null> {
|
||||
const value = await passwords.getPassword(service, storageKey);
|
||||
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
|
||||
const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
|
||||
let clientKeyHalfB64: string | null = null;
|
||||
if (this.clientKeyHalves.has(userId)) {
|
||||
clientKeyHalfB64 = Utils.fromBufferToB64(this.clientKeyHalves.get(userId));
|
||||
}
|
||||
|
||||
if (value == null || value == "") {
|
||||
return null;
|
||||
} else if (!EncString.isSerializedEncString(value)) {
|
||||
// Update to format encrypted with client key half
|
||||
const storageDetails = await this.getStorageDetails({
|
||||
clientKeyHalfB64,
|
||||
clientKeyHalfB64: clientKeyHalfB64,
|
||||
});
|
||||
|
||||
await biometrics.setBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
value,
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
return value;
|
||||
return SymmetricCryptoKey.fromString(value);
|
||||
} else {
|
||||
const encValue = new EncString(value);
|
||||
this.setIv(encValue.iv);
|
||||
const storageDetails = await this.getStorageDetails({
|
||||
clientKeyHalfB64,
|
||||
clientKeyHalfB64: clientKeyHalfB64,
|
||||
});
|
||||
return await biometrics.getBiometricSecret(service, storageKey, storageDetails.key_material);
|
||||
return SymmetricCryptoKey.fromString(
|
||||
await biometrics.getBiometricSecret(
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
storageDetails.key_material,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async setBiometricKey(
|
||||
service: string,
|
||||
storageKey: string,
|
||||
value: string,
|
||||
clientKeyPartB64: string | undefined,
|
||||
): Promise<void> {
|
||||
const parsedValue = SymmetricCryptoKey.fromString(value);
|
||||
if (await this.valueUpToDate({ value: parsedValue, clientKeyPartB64, service, storageKey })) {
|
||||
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
|
||||
const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
|
||||
|
||||
if (
|
||||
await this.valueUpToDate({
|
||||
value: key,
|
||||
clientKeyPartB64: Utils.fromBufferToB64(clientKeyHalf),
|
||||
service: SERVICE,
|
||||
storageKey: getLookupKeyForUser(userId),
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
|
||||
const storageDetails = await this.getStorageDetails({
|
||||
clientKeyHalfB64: Utils.fromBufferToB64(clientKeyHalf),
|
||||
});
|
||||
const storedValue = await biometrics.setBiometricSecret(
|
||||
service,
|
||||
storageKey,
|
||||
value,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
key.toBase64(),
|
||||
storageDetails.key_material,
|
||||
storageDetails.ivB64,
|
||||
);
|
||||
const parsedStoredValue = new EncString(storedValue);
|
||||
await this.storeValueWitness(
|
||||
parsedValue,
|
||||
key,
|
||||
parsedStoredValue,
|
||||
service,
|
||||
storageKey,
|
||||
clientKeyPartB64,
|
||||
SERVICE,
|
||||
getLookupKeyForUser(userId),
|
||||
Utils.fromBufferToB64(clientKeyHalf),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteBiometricKey(service: string, key: string): Promise<void> {
|
||||
await passwords.deletePassword(service, key);
|
||||
await passwords.deletePassword(service, key + KEY_WITNESS_SUFFIX);
|
||||
async deleteBiometricKey(userId: UserId): Promise<void> {
|
||||
await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId));
|
||||
await passwords.deletePassword(SERVICE, getLookupKeyForUser(userId) + KEY_WITNESS_SUFFIX);
|
||||
}
|
||||
|
||||
async authenticateBiometric(): Promise<boolean> {
|
||||
@@ -240,13 +264,58 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
|
||||
return result;
|
||||
}
|
||||
|
||||
async osBiometricsNeedsSetup() {
|
||||
async needsSetup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsCanAutoSetup(): Promise<boolean> {
|
||||
async canAutoSetup(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async osBiometricsSetup(): Promise<void> {}
|
||||
async runSetup(): Promise<void> {}
|
||||
|
||||
async getOrCreateBiometricEncryptionClientKeyHalf(
|
||||
userId: UserId,
|
||||
key: SymmetricCryptoKey,
|
||||
): Promise<Uint8Array | null> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.clientKeyHalves.has(userId)) {
|
||||
return this.clientKeyHalves.get(userId);
|
||||
}
|
||||
|
||||
// Retrieve existing key half if it exists
|
||||
let clientKeyHalf: Uint8Array | null = null;
|
||||
const encryptedClientKeyHalf =
|
||||
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
if (encryptedClientKeyHalf != null) {
|
||||
clientKeyHalf = await this.encryptService.decryptBytes(encryptedClientKeyHalf, key);
|
||||
}
|
||||
if (clientKeyHalf == null) {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
const encKey = await this.encryptService.encryptBytes(keyBytes, key);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
this.clientKeyHalves.set(userId, clientKeyHalf);
|
||||
|
||||
return clientKeyHalf;
|
||||
}
|
||||
|
||||
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return BiometricsStatus.Available;
|
||||
}
|
||||
|
||||
if (this.clientKeyHalves.has(userId)) {
|
||||
return BiometricsStatus.Available;
|
||||
} else {
|
||||
return BiometricsStatus.UnlockNeeded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
export interface OsBiometricService {
|
||||
osSupportsBiometric(): Promise<boolean>;
|
||||
supportsBiometrics(): Promise<boolean>;
|
||||
/**
|
||||
* Check whether support for biometric unlock requires setup. This can be automatic or manual.
|
||||
*
|
||||
* @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
|
||||
*/
|
||||
osBiometricsNeedsSetup: () => Promise<boolean>;
|
||||
needsSetup(): Promise<boolean>;
|
||||
/**
|
||||
* Check whether biometrics can be automatically setup, or requires user interaction.
|
||||
*
|
||||
* @returns true if biometrics support can be automatically setup, false if it requires user interaction.
|
||||
*/
|
||||
osBiometricsCanAutoSetup: () => Promise<boolean>;
|
||||
canAutoSetup(): Promise<boolean>;
|
||||
/**
|
||||
* Starts automatic biometric setup, which places the required configuration files / changes the required settings.
|
||||
*/
|
||||
osBiometricsSetup: () => Promise<void>;
|
||||
runSetup(): Promise<void>;
|
||||
authenticateBiometric(): Promise<boolean>;
|
||||
getBiometricKey(
|
||||
service: string,
|
||||
key: string,
|
||||
clientKeyHalfB64: string | undefined,
|
||||
): Promise<string | null>;
|
||||
setBiometricKey(
|
||||
service: string,
|
||||
key: string,
|
||||
value: string,
|
||||
clientKeyHalfB64: string | undefined,
|
||||
): Promise<void>;
|
||||
deleteBiometricKey(service: string, key: string): Promise<void>;
|
||||
getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null>;
|
||||
setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void>;
|
||||
deleteBiometricKey(userId: UserId): Promise<void>;
|
||||
getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus>;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,14 @@ export class RendererBiometricsService extends DesktopBiometricsService {
|
||||
return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id);
|
||||
}
|
||||
|
||||
async setBiometricProtectedUnlockKeyForUser(userId: UserId, value: string): Promise<void> {
|
||||
return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser(userId, value);
|
||||
async setBiometricProtectedUnlockKeyForUser(
|
||||
userId: UserId,
|
||||
value: SymmetricCryptoKey,
|
||||
): Promise<void> {
|
||||
return await ipc.keyManagement.biometric.setBiometricProtectedUnlockKeyForUser(
|
||||
userId,
|
||||
value.toBase64(),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteBiometricUnlockKeyForUser(userId: UserId): Promise<void> {
|
||||
@@ -46,10 +52,6 @@ export class RendererBiometricsService extends DesktopBiometricsService {
|
||||
return await ipc.keyManagement.biometric.setupBiometrics();
|
||||
}
|
||||
|
||||
async setClientKeyHalfForUser(userId: UserId, value: string | null): Promise<void> {
|
||||
return await ipc.keyManagement.biometric.setClientKeyHalf(userId, value);
|
||||
}
|
||||
|
||||
async getShouldAutopromptNow(): Promise<boolean> {
|
||||
return await ipc.keyManagement.biometric.getShouldAutoprompt();
|
||||
}
|
||||
|
||||
@@ -9,14 +9,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { BiometricStateService, KdfConfigService } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
makeEncString,
|
||||
makeStaticByteArray,
|
||||
makeSymmetricCryptoKey,
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
@@ -80,7 +77,6 @@ describe("ElectronKeyService", () => {
|
||||
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setClientKeyHalfForUser).not.toHaveBeenCalled();
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
@@ -96,14 +92,12 @@ describe("ElectronKeyService", () => {
|
||||
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(mockUserId, null);
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey.keyB64,
|
||||
userKey,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
|
||||
describe("require password on start enabled", () => {
|
||||
@@ -111,73 +105,11 @@ describe("ElectronKeyService", () => {
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("sets new biometric client key half and biometric unlock key when no biometric client key half stored", async () => {
|
||||
const clientKeyHalfBytes = makeStaticByteArray(32);
|
||||
const clientKeyHalf = Utils.fromBufferToUtf8(clientKeyHalfBytes);
|
||||
const encryptedClientKeyHalf = makeEncString();
|
||||
biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(null);
|
||||
cryptoFunctionService.randomBytes.mockResolvedValue(
|
||||
clientKeyHalfBytes.buffer as CsprngArray,
|
||||
);
|
||||
encryptService.encryptString.mockResolvedValue(encryptedClientKeyHalf);
|
||||
|
||||
it("sets biometric key", async () => {
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
clientKeyHalf,
|
||||
);
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey.keyB64,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).toHaveBeenCalledWith(
|
||||
encryptedClientKeyHalf,
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(cryptoFunctionService.randomBytes).toHaveBeenCalledWith(32);
|
||||
expect(encryptService.encryptString).toHaveBeenCalledWith(clientKeyHalf, userKey);
|
||||
});
|
||||
|
||||
it("sets decrypted biometric client key half and biometric unlock key when existing biometric client key half stored", async () => {
|
||||
const encryptedClientKeyHalf = makeEncString();
|
||||
const clientKeyHalf = Utils.fromBufferToUtf8(makeStaticByteArray(32));
|
||||
biometricStateService.getEncryptedClientKeyHalf.mockResolvedValue(
|
||||
encryptedClientKeyHalf,
|
||||
);
|
||||
encryptService.decryptString.mockResolvedValue(clientKeyHalf);
|
||||
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setClientKeyHalfForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
clientKeyHalf,
|
||||
);
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey.keyB64,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getRequirePasswordOnStart).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(biometricStateService.getEncryptedClientKeyHalf).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
);
|
||||
expect(encryptService.decryptString).toHaveBeenCalledWith(
|
||||
encryptedClientKeyHalf,
|
||||
userKey,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,9 +8,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CsprngString } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
@@ -77,10 +75,7 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
}
|
||||
|
||||
private async storeBiometricsProtectedUserKey(userKey: UserKey, userId: UserId): Promise<void> {
|
||||
// May resolve to null, in which case no client key have is required
|
||||
const clientEncKeyHalf = await this.getBiometricEncryptionClientKeyHalf(userKey, userId);
|
||||
await this.biometricService.setClientKeyHalfForUser(userId, clientEncKeyHalf);
|
||||
await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey.keyB64);
|
||||
await this.biometricService.setBiometricProtectedUnlockKeyForUser(userId, userKey);
|
||||
}
|
||||
|
||||
protected async shouldStoreKey(keySuffix: KeySuffixOptions, userId: UserId): Promise<boolean> {
|
||||
@@ -91,34 +86,4 @@ export class ElectronKeyService extends DefaultKeyService {
|
||||
await this.biometricService.deleteBiometricUnlockKeyForUser(userId);
|
||||
await super.clearAllStoredUserKeys(userId);
|
||||
}
|
||||
|
||||
private async getBiometricEncryptionClientKeyHalf(
|
||||
userKey: UserKey,
|
||||
userId: UserId,
|
||||
): Promise<CsprngString | null> {
|
||||
const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId);
|
||||
if (!requireClientKeyHalf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Retrieve existing key half if it exists
|
||||
let clientKeyHalf: CsprngString | null = null;
|
||||
const encryptedClientKeyHalf =
|
||||
await this.biometricStateService.getEncryptedClientKeyHalf(userId);
|
||||
if (encryptedClientKeyHalf != null) {
|
||||
clientKeyHalf = (await this.encryptService.decryptString(
|
||||
encryptedClientKeyHalf,
|
||||
userKey,
|
||||
)) as CsprngString;
|
||||
}
|
||||
if (clientKeyHalf == null) {
|
||||
// Set a key half if it doesn't exist
|
||||
const keyBytes = await this.cryptoFunctionService.randomBytes(32);
|
||||
clientKeyHalf = Utils.fromBufferToUtf8(keyBytes) as CsprngString;
|
||||
const encKey = await this.encryptService.encryptString(clientKeyHalf, userKey);
|
||||
await this.biometricStateService.setEncryptedClientKeyHalf(encKey, userId);
|
||||
}
|
||||
|
||||
return clientKeyHalf;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,13 @@ const biometric = {
|
||||
action: BiometricAction.GetStatusForUser,
|
||||
userId: userId,
|
||||
} satisfies BiometricMessage),
|
||||
setBiometricProtectedUnlockKeyForUser: (userId: string, value: string): Promise<void> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
setBiometricProtectedUnlockKeyForUser: (userId: string, keyB64: string): Promise<void> => {
|
||||
return ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.SetKeyForUser,
|
||||
userId: userId,
|
||||
key: value,
|
||||
} satisfies BiometricMessage),
|
||||
key: keyB64,
|
||||
} satisfies BiometricMessage);
|
||||
},
|
||||
deleteBiometricUnlockKeyForUser: (userId: string): Promise<void> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.RemoveKeyForUser,
|
||||
@@ -40,12 +41,6 @@ const biometric = {
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.Setup,
|
||||
} satisfies BiometricMessage),
|
||||
setClientKeyHalf: (userId: string, value: string | null): Promise<void> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.SetClientKeyHalf,
|
||||
userId: userId,
|
||||
key: value,
|
||||
} satisfies BiometricMessage),
|
||||
getShouldAutoprompt: (): Promise<boolean> =>
|
||||
ipcRenderer.invoke("biometric", {
|
||||
action: BiometricAction.GetShouldAutoprompt,
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
"typeIdentity": {
|
||||
"message": "Identity"
|
||||
},
|
||||
"typeNote": {
|
||||
"message": "Note"
|
||||
},
|
||||
"typeSecureNote": {
|
||||
"message": "Secure note"
|
||||
},
|
||||
@@ -3812,5 +3815,139 @@
|
||||
"message": "Learn more about SSH agent",
|
||||
"description": "Two part message",
|
||||
"example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent"
|
||||
},
|
||||
"assignToCollections": {
|
||||
"message": "Assign to collections"
|
||||
},
|
||||
"assignToTheseCollections": {
|
||||
"message": "Assign to these collections"
|
||||
},
|
||||
"bulkCollectionAssignmentDialogDescriptionSingular": {
|
||||
"message": "Only organization members with access to these collections will be able to see the item."
|
||||
},
|
||||
"bulkCollectionAssignmentDialogDescriptionPlural": {
|
||||
"message": "Only organization members with access to these collections will be able to see the items."
|
||||
},
|
||||
"noCollectionsAssigned": {
|
||||
"message": "No collections have been assigned"
|
||||
},
|
||||
"assign": {
|
||||
"message": "Assign"
|
||||
},
|
||||
"bulkCollectionAssignmentDialogDescription": {
|
||||
"message": "Only organization members with access to these collections will be able to see the items."
|
||||
},
|
||||
"bulkCollectionAssignmentWarning": {
|
||||
"message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.",
|
||||
"placeholders": {
|
||||
"total_count": {
|
||||
"content": "$1",
|
||||
"example": "10"
|
||||
},
|
||||
"readonly_count": {
|
||||
"content": "$2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selectCollectionsToAssign": {
|
||||
"message": "Select collections to assign"
|
||||
},
|
||||
"personalItemsTransferWarning": {
|
||||
"message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.",
|
||||
"placeholders": {
|
||||
"personal_items_count": {
|
||||
"content": "$1",
|
||||
"example": "2 items"
|
||||
}
|
||||
}
|
||||
},
|
||||
"personalItemsWithOrgTransferWarning": {
|
||||
"message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.",
|
||||
"placeholders": {
|
||||
"personal_items_count": {
|
||||
"content": "$1",
|
||||
"example": "2 items"
|
||||
},
|
||||
"org": {
|
||||
"content": "$2",
|
||||
"example": "Organization name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"personalItemTransferWarningSingular": {
|
||||
"message": "1 item will be permanently transferred to the selected organization. You will no longer own this item."
|
||||
},
|
||||
"personalItemWithOrgTransferWarningSingular": {
|
||||
"message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.",
|
||||
"placeholders": {
|
||||
"org": {
|
||||
"content": "$1",
|
||||
"example": "Organization name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"successfullyAssignedCollections": {
|
||||
"message": "Successfully assigned collections"
|
||||
},
|
||||
"nothingSelected": {
|
||||
"message": "You have not selected anything."
|
||||
},
|
||||
"itemsMovedToOrg": {
|
||||
"message": "Items moved to $ORGNAME$",
|
||||
"placeholders": {
|
||||
"orgname": {
|
||||
"content": "$1",
|
||||
"example": "Company Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"itemMovedToOrg": {
|
||||
"message": "Item moved to $ORGNAME$",
|
||||
"placeholders": {
|
||||
"orgname": {
|
||||
"content": "$1",
|
||||
"example": "Company Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"movedItemsToOrg": {
|
||||
"message": "Selected items moved to $ORGNAME$",
|
||||
"placeholders": {
|
||||
"orgname": {
|
||||
"content": "$1",
|
||||
"example": "Company Name"
|
||||
}
|
||||
},
|
||||
"personalItemsTransferWarningPlural": {
|
||||
"message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.",
|
||||
"placeholders": {
|
||||
"personal_items_count": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"personalItemWithOrgTransferWarningSingular": {
|
||||
"message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.",
|
||||
"placeholders": {
|
||||
"org": {
|
||||
"content": "$1",
|
||||
"example": "Organization name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"personalItemsWithOrgTransferWarningPlural": {
|
||||
"message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.",
|
||||
"placeholders": {
|
||||
"personal_items_count": {
|
||||
"content": "$1",
|
||||
"example": "2"
|
||||
},
|
||||
"org": {
|
||||
"content": "$2",
|
||||
"example": "Organization name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Subject, firstValueFrom } from "rxjs";
|
||||
import { SsoUrlService } from "@bitwarden/auth/common";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
import { RegionConfig } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Message, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- For dependency creation
|
||||
@@ -187,14 +188,19 @@ export class Main {
|
||||
|
||||
this.desktopSettingsService = new DesktopSettingsService(stateProvider);
|
||||
const biometricStateService = new DefaultBiometricStateService(stateProvider);
|
||||
|
||||
const encryptService = new EncryptServiceImplementation(
|
||||
this.mainCryptoFunctionService,
|
||||
this.logService,
|
||||
true,
|
||||
);
|
||||
this.biometricsService = new MainBiometricsService(
|
||||
this.i18nService,
|
||||
this.windowMain,
|
||||
this.logService,
|
||||
this.messagingService,
|
||||
process.platform,
|
||||
biometricStateService,
|
||||
encryptService,
|
||||
this.mainCryptoFunctionService,
|
||||
);
|
||||
|
||||
this.windowMain = new WindowMain(
|
||||
|
||||
@@ -9,8 +9,6 @@ export enum BiometricAction {
|
||||
SetKeyForUser = "setKeyForUser",
|
||||
RemoveKeyForUser = "removeKeyForUser",
|
||||
|
||||
SetClientKeyHalf = "setClientKeyHalf",
|
||||
|
||||
Setup = "setup",
|
||||
|
||||
GetShouldAutoprompt = "getShouldAutoprompt",
|
||||
@@ -18,21 +16,13 @@ export enum BiometricAction {
|
||||
}
|
||||
|
||||
export type BiometricMessage =
|
||||
| {
|
||||
action: BiometricAction.SetClientKeyHalf;
|
||||
userId: string;
|
||||
key: string | null;
|
||||
}
|
||||
| {
|
||||
action: BiometricAction.SetKeyForUser;
|
||||
userId: string;
|
||||
key: string;
|
||||
}
|
||||
| {
|
||||
action: Exclude<
|
||||
BiometricAction,
|
||||
BiometricAction.SetClientKeyHalf | BiometricAction.SetKeyForUser
|
||||
>;
|
||||
action: Exclude<BiometricAction, BiometricAction.SetKeyForUser>;
|
||||
userId?: string;
|
||||
data?: any;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
|
||||
<label for="type">{{ "type" | i18n }}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type" (change)="typeChange()">
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||
<option *ngFor="let item of menuItems$ | async" [ngValue]="item.type">
|
||||
{{ item.labelKey | i18n }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { NgForm } from "@angular/forms";
|
||||
import { map, shareReplay } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
|
||||
@@ -22,6 +23,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
@@ -35,6 +38,18 @@ const BroadcasterSubscriptionId = "AddEditComponent";
|
||||
export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@ViewChild("form")
|
||||
private form: NgForm;
|
||||
menuItems$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedItemTypes) =>
|
||||
// Filter out restricted item types from the default CIPHER_MENU_ITEMS array
|
||||
CIPHER_MENU_ITEMS.filter(
|
||||
(typeOption) =>
|
||||
!restrictedItemTypes.some(
|
||||
(restrictedType) => restrictedType.cipherType === typeOption.type,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
@@ -59,6 +74,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
||||
cipherAuthorizationService: CipherAuthorizationService,
|
||||
sdkService: SdkService,
|
||||
sshImportPromptService: SshImportPromptService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<bit-dialog dialogSize="large">
|
||||
<span bitDialogTitle>
|
||||
{{ "assignToCollections" | i18n }}
|
||||
<span class="tw-text-sm tw-normal-case tw-text-muted">
|
||||
{{ editableItemCount | pluralize: ("item" | i18n) : ("items" | i18n) }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div bitDialogContent>
|
||||
<assign-collections
|
||||
[params]="params"
|
||||
[submitBtn]="assignSubmitButton"
|
||||
(onCollectionAssign)="onCollectionAssign($event)"
|
||||
(editableItemCountChange)="editableItemCount = $event"
|
||||
></assign-collections>
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
#assignSubmitButton
|
||||
form="assign_collections_form"
|
||||
type="submit"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "assign" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { PluralizePipe } from "@bitwarden/angular/pipes/pluralize.pipe";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
AssignCollectionsComponent,
|
||||
CollectionAssignmentParams,
|
||||
CollectionAssignmentResult,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
templateUrl: "./assign-collections-desktop.component.html",
|
||||
imports: [AssignCollectionsComponent, PluralizePipe, DialogModule, ButtonModule, JslibModule],
|
||||
})
|
||||
export class AssignCollectionsDesktopComponent {
|
||||
protected editableItemCount: number;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) public params: CollectionAssignmentParams,
|
||||
private dialogRef: DialogRef<CollectionAssignmentResult>,
|
||||
) {}
|
||||
|
||||
protected async onCollectionAssign(result: CollectionAssignmentResult) {
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<CollectionAssignmentParams>) {
|
||||
return dialogService.open<CollectionAssignmentResult, CollectionAssignmentParams>(
|
||||
AssignCollectionsDesktopComponent,
|
||||
config,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./assign-collections-desktop.component";
|
||||
@@ -36,7 +36,7 @@
|
||||
class="primary"
|
||||
(click)="restore()"
|
||||
appA11yTitle="{{ 'restore' | i18n }}"
|
||||
*ngIf="cipher.isDeleted"
|
||||
*ngIf="cipher.isDeleted && cipher.permissions.restore"
|
||||
>
|
||||
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
@@ -50,7 +50,7 @@
|
||||
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ng-container>
|
||||
<div class="right" *ngIf="((canDeleteCipher$ | async) && action === 'edit') || action === 'view'">
|
||||
<div class="right" *ngIf="cipher.permissions.delete && (action === 'edit' || action === 'view')">
|
||||
<button
|
||||
type="button"
|
||||
(click)="delete()"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Input, Output, EventEmitter, Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { Observable, firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -25,6 +25,7 @@ export class ItemFooterComponent implements OnInit {
|
||||
@Input() collectionId: string | null = null;
|
||||
@Input({ required: true }) action: string = "view";
|
||||
@Input() isSubmitting: boolean = false;
|
||||
@Input() masterPasswordAlreadyPrompted: boolean = false;
|
||||
@Output() onEdit = new EventEmitter<CipherView>();
|
||||
@Output() onClone = new EventEmitter<CipherView>();
|
||||
@Output() onDelete = new EventEmitter<CipherView>();
|
||||
@@ -32,10 +33,8 @@ export class ItemFooterComponent implements OnInit {
|
||||
@Output() onCancel = new EventEmitter<CipherView>();
|
||||
@ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null;
|
||||
|
||||
canDeleteCipher$: Observable<boolean> = new Observable();
|
||||
activeUserId: UserId | null = null;
|
||||
|
||||
private passwordReprompted = false;
|
||||
passwordReprompted: boolean = false;
|
||||
|
||||
constructor(
|
||||
protected cipherService: CipherService,
|
||||
@@ -49,8 +48,8 @@ export class ItemFooterComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher);
|
||||
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
this.passwordReprompted = this.masterPasswordAlreadyPrompted;
|
||||
}
|
||||
|
||||
async clone() {
|
||||
|
||||
@@ -20,78 +20,20 @@
|
||||
</h2>
|
||||
</div>
|
||||
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
|
||||
<li
|
||||
class="filter-option"
|
||||
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Login }"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter(cipherTypeEnum.Login)"
|
||||
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.Login"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i> {{ "typeLogin" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter(cipherTypeEnum.Card)"
|
||||
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.Card"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i> {{ "typeCard" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="filter-option"
|
||||
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Identity }"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter(cipherTypeEnum.Identity)"
|
||||
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.Identity"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i> {{ "typeIdentity" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="filter-option"
|
||||
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SecureNote }"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter(cipherTypeEnum.SecureNote)"
|
||||
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.SecureNote"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i> {{
|
||||
"typeSecureNote" | i18n
|
||||
}}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
class="filter-option"
|
||||
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SshKey }"
|
||||
>
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter(cipherTypeEnum.SshKey)"
|
||||
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.SshKey"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i> {{ "typeSshKey" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
@for (typeFilter of typeFilters$ | async; track typeFilter) {
|
||||
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === typeFilter.type }">
|
||||
<span class="filter-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="filter-button"
|
||||
(click)="applyFilter(typeFilter.type)"
|
||||
[attr.aria-pressed]="activeFilter.cipherType === typeFilter.type"
|
||||
>
|
||||
<i class="bwi bwi-fw {{ typeFilter.icon }}" aria-hidden="true"></i> {{
|
||||
typeFilter.labelKey | i18n
|
||||
}}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { map, shareReplay } from "rxjs";
|
||||
|
||||
import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/type-filter.component";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
|
||||
|
||||
@Component({
|
||||
selector: "app-type-filter",
|
||||
@@ -8,7 +11,22 @@ import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angul
|
||||
standalone: false,
|
||||
})
|
||||
export class TypeFilterComponent extends BaseTypeFilterComponent {
|
||||
constructor() {
|
||||
protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe(
|
||||
map((restrictedItemTypes) =>
|
||||
// Filter out restricted item types from the typeFilters array
|
||||
CIPHER_MENU_ITEMS.filter(
|
||||
(typeFilter) =>
|
||||
!restrictedItemTypes.some(
|
||||
(restrictedType) =>
|
||||
restrictedType.allowViewOrgIds.length === 0 &&
|
||||
restrictedType.cipherType === typeFilter.type,
|
||||
),
|
||||
),
|
||||
),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
constructor(private restrictedItemTypesService: RestrictedItemTypesService) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,25 +72,11 @@
|
||||
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #addCipherMenu>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
|
||||
<i class="bwi bwi-globe tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "typeLogin" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
|
||||
<i class="bwi bwi-credit-card tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "typeCard" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
|
||||
<i class="bwi bwi-id-card tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "typeIdentity" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
|
||||
<i class="bwi bwi-sticky-note tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "typeSecureNote" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.SshKey)">
|
||||
<i class="bwi bwi-key tw-mr-1" aria-hidden="true"></i>
|
||||
{{ "typeSshKey" | i18n }}
|
||||
</button>
|
||||
@for (itemTypes of itemTypes$ | async; track itemTypes.type) {
|
||||
<button type="button" bitMenuItem (click)="addCipher(itemTypes.type)">
|
||||
<i class="bwi {{ itemTypes.icon }} tw-mr-1" aria-hidden="true"></i>
|
||||
{{ itemTypes.labelKey | i18n }}
|
||||
</button>
|
||||
}
|
||||
</bit-menu>
|
||||
</ng-template>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { MenuModule } from "@bitwarden/components";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
@@ -25,8 +26,9 @@ export class VaultItemsV2Component extends BaseVaultItemsComponent {
|
||||
private readonly searchBarService: SearchBarService,
|
||||
cipherService: CipherService,
|
||||
accountService: AccountService,
|
||||
restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
super(searchService, cipherService, accountService);
|
||||
super(searchService, cipherService, accountService, restrictedItemTypesService);
|
||||
|
||||
this.searchBarService.searchText$
|
||||
.pipe(distinctUntilChanged(), takeUntilDestroyed())
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
|
||||
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
|
||||
|
||||
@@ -22,8 +23,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent {
|
||||
searchBarService: SearchBarService,
|
||||
cipherService: CipherService,
|
||||
accountService: AccountService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {
|
||||
super(searchService, cipherService, accountService);
|
||||
super(searchService, cipherService, accountService, restrictedItemTypesService);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
(onDelete)="deleteCipher()"
|
||||
(onCancel)="cancelCipher($event)"
|
||||
[isSubmitting]="isSubmitting"
|
||||
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
|
||||
></app-vault-item-footer>
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
|
||||
@@ -9,16 +9,16 @@ import {
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom } from "rxjs";
|
||||
import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observable } from "rxjs";
|
||||
import { filter, map, take } from "rxjs/operators";
|
||||
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
|
||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
@@ -43,6 +43,8 @@ import {
|
||||
DialogService,
|
||||
ItemModule,
|
||||
ToastService,
|
||||
CopyClickListener,
|
||||
COPY_CLICK_LISTENER,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import {
|
||||
@@ -57,10 +59,12 @@ import {
|
||||
CipherFormMode,
|
||||
CipherFormModule,
|
||||
CipherViewComponent,
|
||||
CollectionAssignmentResult,
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultCipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
CipherFormComponent,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { NavComponent } from "../../../app/layout/nav.component";
|
||||
@@ -69,6 +73,7 @@ import { DesktopCredentialGenerationService } from "../../../services/desktop-ci
|
||||
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
|
||||
import { AssignCollectionsDesktopComponent } from "./assign-collections";
|
||||
import { ItemFooterComponent } from "./item-footer.component";
|
||||
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
@@ -110,15 +115,21 @@ const BroadcasterSubscriptionId = "VaultComponent";
|
||||
useClass: DesktopPremiumUpgradePromptService,
|
||||
},
|
||||
{ provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService },
|
||||
{
|
||||
provide: COPY_CLICK_LISTENER,
|
||||
useExisting: VaultV2Component,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultV2Component implements OnInit, OnDestroy {
|
||||
export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener {
|
||||
@ViewChild(VaultItemsV2Component, { static: true })
|
||||
vaultItemsComponent: VaultItemsV2Component | null = null;
|
||||
@ViewChild(VaultFilterComponent, { static: true })
|
||||
vaultFilterComponent: VaultFilterComponent | null = null;
|
||||
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
|
||||
folderAddEditModalRef: ViewContainerRef | null = null;
|
||||
@ViewChild(CipherFormComponent)
|
||||
cipherFormComponent: CipherFormComponent | null = null;
|
||||
|
||||
action: CipherFormMode | "view" | null = null;
|
||||
cipherId: string | null = null;
|
||||
@@ -142,6 +153,11 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
config: CipherFormConfig | null = null;
|
||||
isSubmitting = false;
|
||||
|
||||
private organizations$: Observable<Organization[]> = this.accountService.activeAccount$.pipe(
|
||||
map((a) => a?.id),
|
||||
switchMap((id) => this.organizationService.organizations$(id)),
|
||||
);
|
||||
|
||||
protected canAccessAttachments$ = this.accountService.activeAccount$.pipe(
|
||||
filter((account): account is Account => !!account),
|
||||
switchMap((account) =>
|
||||
@@ -149,14 +165,14 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
),
|
||||
);
|
||||
|
||||
private modal: ModalRef | null = null;
|
||||
private componentIsDestroyed$ = new Subject<boolean>();
|
||||
private allOrganizations: Organization[] = [];
|
||||
private allCollections: CollectionView[] = [];
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private ngZone: NgZone,
|
||||
@@ -176,6 +192,7 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
private formConfigService: CipherFormConfigService,
|
||||
private premiumUpgradePromptService: PremiumUpgradePromptService,
|
||||
private collectionService: CollectionService,
|
||||
private organizationService: OrganizationService,
|
||||
private folderService: FolderService,
|
||||
) {}
|
||||
|
||||
@@ -312,6 +329,16 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.organizations$.pipe(takeUntil(this.componentIsDestroyed$)).subscribe((orgs) => {
|
||||
this.allOrganizations = orgs;
|
||||
});
|
||||
|
||||
this.collectionService.decryptedCollections$
|
||||
.pipe(takeUntil(this.componentIsDestroyed$))
|
||||
.subscribe((collections) => {
|
||||
this.allCollections = collections;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -353,6 +380,13 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for Vault level CopyClickDirectives to send the minimizeOnCopy message
|
||||
*/
|
||||
onCopy() {
|
||||
this.messagingService.send("minimizeOnCopy");
|
||||
}
|
||||
|
||||
async viewCipher(cipher: CipherView) {
|
||||
if (await this.shouldReprompt(cipher, "view")) {
|
||||
return;
|
||||
@@ -364,7 +398,14 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
cipher.collectionIds.includes(c.id),
|
||||
) ?? null;
|
||||
this.action = "view";
|
||||
|
||||
await this.go().catch(() => {});
|
||||
await this.eventCollectionService.collect(
|
||||
EventType.Cipher_ClientViewed,
|
||||
cipher.id,
|
||||
false,
|
||||
cipher.organizationId,
|
||||
);
|
||||
}
|
||||
|
||||
async openAttachmentsDialog() {
|
||||
@@ -381,6 +422,26 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
result?.action === AttachmentDialogResult.Uploaded
|
||||
) {
|
||||
await this.vaultItemsComponent?.refresh().catch(() => {});
|
||||
|
||||
if (this.cipherFormComponent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedCipher = await this.cipherService.get(
|
||||
this.cipherId as CipherId,
|
||||
this.activeUserId as UserId,
|
||||
);
|
||||
const updatedCipherView = await this.cipherService.decrypt(
|
||||
updatedCipher,
|
||||
this.activeUserId as UserId,
|
||||
);
|
||||
|
||||
this.cipherFormComponent.patchCipher((currentCipher) => {
|
||||
currentCipher.attachments = updatedCipherView.attachments;
|
||||
currentCipher.revisionDate = updatedCipherView.revisionDate;
|
||||
|
||||
return currentCipher;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,6 +481,16 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (cipher.canAssignToCollections) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("assignToCollections"),
|
||||
click: () =>
|
||||
this.functionWithChangeDetection(async () => {
|
||||
await this.shareCipher(cipher);
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switch (cipher.type) {
|
||||
@@ -531,6 +602,36 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
async shareCipher(cipher: CipherView) {
|
||||
if (!cipher) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("nothingSelected"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await this.passwordReprompt(cipher))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableCollections = this.getAvailableCollections(cipher);
|
||||
|
||||
const dialog = AssignCollectionsDesktopComponent.open(this.dialogService, {
|
||||
data: {
|
||||
ciphers: [cipher],
|
||||
organizationId: cipher.organizationId as OrganizationId,
|
||||
availableCollections,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
if (result === CollectionAssignmentResult.Saved) {
|
||||
await this.savedCipher(cipher);
|
||||
}
|
||||
}
|
||||
|
||||
async addCipher(type: CipherType) {
|
||||
if (this.action === "add") {
|
||||
return;
|
||||
@@ -603,6 +704,16 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
await this.go().catch(() => {});
|
||||
}
|
||||
|
||||
private getAvailableCollections(cipher: CipherView): CollectionView[] {
|
||||
const orgId = cipher.organizationId;
|
||||
if (!orgId || orgId === "MyVault") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const organization = this.allOrganizations.find((o) => o.id === orgId);
|
||||
return this.allCollections.filter((c) => c.organizationId === organization?.id && !c.readOnly);
|
||||
}
|
||||
|
||||
private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
|
||||
if (vaultFilter.status === "favorites") {
|
||||
return "searchFavorites";
|
||||
@@ -633,10 +744,17 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async editFolder(folderId: string) {
|
||||
if (!this.activeUserId) {
|
||||
return;
|
||||
}
|
||||
const folderView = await firstValueFrom(
|
||||
this.folderService.getDecrypted$(folderId, this.activeUserId),
|
||||
);
|
||||
|
||||
if (!folderView) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, {
|
||||
editFolderConfig: {
|
||||
folder: {
|
||||
@@ -651,7 +769,7 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
result === AddEditFolderDialogResult.Deleted ||
|
||||
result === AddEditFolderDialogResult.Created
|
||||
) {
|
||||
await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter);
|
||||
await this.vaultFilterComponent?.reloadCollectionsAndFolders(this.activeFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,10 +814,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
private addCipherWithChangeDetection(type: CipherType) {
|
||||
this.functionWithChangeDetection(() => this.addCipher(type).catch(() => {}));
|
||||
}
|
||||
|
||||
private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) {
|
||||
this.functionWithChangeDetection(() => {
|
||||
(async () => {
|
||||
|
||||
@@ -11,8 +11,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<p *ngIf="!dataSource.filteredData.length">{{ "noGroupsInList" | i18n }}</p>
|
||||
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||
from overflowing the <main> element. -->
|
||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<bit-table *ngIf="dataSource.filteredData.length" [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
</bit-callout>
|
||||
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||
from overflowing the <main> element. -->
|
||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
|
||||
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
||||
import { ScrollLayoutDirective } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule } from "../../../shared";
|
||||
import { SharedOrganizationModule } from "../shared";
|
||||
@@ -27,6 +28,7 @@ import { MembersComponent } from "./members.component";
|
||||
PasswordCalloutComponent,
|
||||
ScrollingModule,
|
||||
PasswordStrengthV2Component,
|
||||
ScrollLayoutDirective,
|
||||
],
|
||||
declarations: [
|
||||
BulkConfirmDialogComponent,
|
||||
|
||||
@@ -17,7 +17,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { deepLinkGuard } from "../../auth/guards/deep-link/deep-link.guard";
|
||||
|
||||
import { VaultModule } from "./collections/vault.module";
|
||||
import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard";
|
||||
import { organizationPermissionsGuard } from "./guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "./guards/org-redirect.guard";
|
||||
import { AdminConsoleIntegrationsComponent } from "./integrations/integrations.component";
|
||||
@@ -42,10 +41,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "integrations",
|
||||
canActivate: [
|
||||
isEnterpriseOrgGuard(false),
|
||||
organizationPermissionsGuard(canAccessIntegrations),
|
||||
],
|
||||
canActivate: [organizationPermissionsGuard(canAccessIntegrations)],
|
||||
component: AdminConsoleIntegrationsComponent,
|
||||
data: {
|
||||
titleId: "integrations",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { ScrollLayoutDirective } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule } from "../../shared";
|
||||
|
||||
import { CoreOrganizationModule } from "./core";
|
||||
@@ -18,6 +20,7 @@ import { AccessSelectorModule } from "./shared/components/access-selector";
|
||||
OrganizationsRoutingModule,
|
||||
LooseComponentsModule,
|
||||
ScrollingModule,
|
||||
ScrollLayoutDirective,
|
||||
],
|
||||
declarations: [GroupsComponent, GroupAddEditComponent],
|
||||
})
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<!-- Please remove this disable statement when editing this file! -->
|
||||
<!-- eslint-disable tailwindcss/no-custom-classname -->
|
||||
<div class="tw-flex" *ngIf="!hideMultiSelect">
|
||||
<bit-form-field *ngIf="permissionMode == 'edit'" class="tw-mr-3 tw-shrink-0">
|
||||
<bit-form-field *ngIf="permissionMode == 'edit'" class="tw-mr-3 tw-shrink-0 tw-basis-2/5">
|
||||
<bit-label>{{ "permission" | i18n }}</bit-label>
|
||||
<select
|
||||
<bit-select
|
||||
bitInput
|
||||
[disabled]="disabled"
|
||||
[(ngModel)]="initialPermission"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(blur)="handleBlur()"
|
||||
(closed)="handleBlur()"
|
||||
>
|
||||
<option *ngFor="let p of permissionList" [value]="p.perm">
|
||||
{{ p.labelId | i18n }}
|
||||
</option>
|
||||
</select>
|
||||
<bit-option *ngFor="let p of permissionList" [value]="p.perm" [label]="p.labelId | i18n">
|
||||
</bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field class="tw-grow" *ngIf="!disabled">
|
||||
<bit-form-field class="tw-grow tw-p-3" *ngIf="!disabled">
|
||||
<bit-label>{{ selectorLabelText }}</bit-label>
|
||||
<bit-multi-select
|
||||
class="tw-w-full"
|
||||
@@ -51,7 +50,7 @@
|
||||
[formGroupName]="i"
|
||||
[ngClass]="{ 'tw-text-muted': item.readonly }"
|
||||
>
|
||||
<td bitCell [ngSwitch]="item.type">
|
||||
<td bitCell [ngSwitch]="item.type" class="tw-w-5/12">
|
||||
<div class="tw-flex tw-items-center" *ngSwitchCase="itemType.Member">
|
||||
<bit-avatar size="small" class="tw-mr-3" text="{{ item.labelName }}"></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
@@ -79,28 +78,22 @@
|
||||
|
||||
<td bitCell *ngIf="permissionMode != 'hidden'">
|
||||
<ng-container *ngIf="canEditItemPermission(item); else readOnlyPerm">
|
||||
<label class="tw-sr-only" [for]="'permission' + i"
|
||||
>{{ item.labelName }} {{ "permission" | i18n }}</label
|
||||
>
|
||||
<div class="tw-relative tw-inline-block">
|
||||
<select
|
||||
<bit-form-field>
|
||||
<bit-label>{{ item.labelName }} {{ "permission" | i18n }}</bit-label>
|
||||
<bit-select
|
||||
bitInput
|
||||
class="tw-apperance-none -tw-ml-3 tw-max-w-40 tw-appearance-none tw-overflow-ellipsis !tw-rounded tw-border-transparent !tw-bg-transparent tw-pr-6 tw-font-bold hover:tw-border-primary-700"
|
||||
formControlName="permission"
|
||||
[id]="'permission' + i"
|
||||
(blur)="handleBlur()"
|
||||
(closed)="handleBlur()"
|
||||
>
|
||||
<option *ngFor="let p of permissionList" [value]="p.perm">
|
||||
{{ p.labelId | i18n }}
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
[for]="'permission' + i"
|
||||
class="tw-absolute tw-inset-y-0 tw-right-4 tw-mb-0 tw-flex tw-items-center"
|
||||
>
|
||||
<i class="bwi bwi-sm bwi-angle-down tw-leading-[0]"></i>
|
||||
</label>
|
||||
</div>
|
||||
<bit-option
|
||||
*ngFor="let p of permissionList"
|
||||
[value]="p.perm"
|
||||
[label]="p.labelId | i18n"
|
||||
>
|
||||
</bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #readOnlyPerm>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ButtonModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
SelectModule,
|
||||
TableModule,
|
||||
TabsModule,
|
||||
} from "@bitwarden/components";
|
||||
@@ -71,6 +72,7 @@ describe("AccessSelectorComponent", () => {
|
||||
PreloadedEnglishI18nModule,
|
||||
JslibModule,
|
||||
IconButtonModule,
|
||||
SelectModule,
|
||||
],
|
||||
declarations: [TestableAccessSelectorComponent, UserTypePipe],
|
||||
providers: [],
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
SelectModule,
|
||||
TableModule,
|
||||
TabsModule,
|
||||
} from "@bitwarden/components";
|
||||
@@ -47,6 +48,7 @@ export default {
|
||||
TableModule,
|
||||
JslibModule,
|
||||
IconButtonModule,
|
||||
SelectModule,
|
||||
],
|
||||
providers: [],
|
||||
}),
|
||||
|
||||
@@ -591,5 +591,5 @@ export function openCollectionDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<CollectionDialogParams, DialogRef<CollectionDialogResult>>,
|
||||
) {
|
||||
return dialogService.open(CollectionDialogComponent, config);
|
||||
return dialogService.open<CollectionDialogResult>(CollectionDialogComponent, config);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import { Component, inject } from "@angular/core";
|
||||
import { Params } from "@angular/router";
|
||||
|
||||
import { BitwardenLogo } from "@bitwarden/auth/angular";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { OrganizationSponsorshipResponse } from "@bitwarden/common/admin-console/models/response/organization-sponsorship.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { Icons, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
||||
|
||||
@@ -22,7 +21,7 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
||||
standalone: false,
|
||||
})
|
||||
export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent {
|
||||
protected logo = BitwardenLogo;
|
||||
protected logo = Icons.BitwardenLogo;
|
||||
failedShortMessage = "inviteAcceptFailedShort";
|
||||
failedMessage = "inviteAcceptFailed";
|
||||
|
||||
|
||||
@@ -9,7 +9,13 @@ import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { DialogService, ToastService, TableModule, PopoverModule } from "@bitwarden/components";
|
||||
import {
|
||||
DialogService,
|
||||
ToastService,
|
||||
TableModule,
|
||||
PopoverModule,
|
||||
LayoutComponent,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { VaultBannersService } from "../../../vault/individual-vault/vault-banners/services/vault-banners.service";
|
||||
@@ -115,6 +121,12 @@ describe("DeviceManagementComponent", () => {
|
||||
showError: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: LayoutComponent,
|
||||
useValue: {
|
||||
mainContent: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import {
|
||||
AbstractControl,
|
||||
@@ -19,7 +18,10 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
DialogRef,
|
||||
ButtonModule,
|
||||
DialogConfig,
|
||||
DIALOG_DATA,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { formatDate } from "@angular/common";
|
||||
import { Component, OnInit, signal } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
@@ -16,7 +15,7 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AddSponsorshipDialogComponent } from "./add-sponsorship-dialog.component";
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
activeAuthGuard,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
AnonLayoutWrapperData,
|
||||
PasswordHintComponent,
|
||||
RegistrationFinishComponent,
|
||||
RegistrationStartComponent,
|
||||
@@ -22,7 +20,6 @@ import {
|
||||
RegistrationLinkExpiredComponent,
|
||||
LoginComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LockIcon,
|
||||
TwoFactorTimeoutIcon,
|
||||
UserLockIcon,
|
||||
SsoKeyIcon,
|
||||
@@ -39,6 +36,7 @@ import {
|
||||
NewDeviceVerificationComponent,
|
||||
DeviceVerificationIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, Icons } from "@bitwarden/components";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import { VaultIcons } from "@bitwarden/vault";
|
||||
|
||||
@@ -399,7 +397,7 @@ const routes: Routes = [
|
||||
pageTitle: {
|
||||
key: "yourVaultIsLockedV2",
|
||||
},
|
||||
pageIcon: LockIcon,
|
||||
pageIcon: Icons.LockIcon,
|
||||
showReadonlyHostname: true,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -17,7 +16,7 @@ import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response
|
||||
import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view";
|
||||
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { NoItemsModule, ToastService } from "@bitwarden/components";
|
||||
import { AnonLayoutWrapperDataService, NoItemsModule, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { ExpiredSendIcon } from "@bitwarden/send-ui";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" scrollWindow class="tw-pb-8">
|
||||
<cdk-virtual-scroll-viewport [itemSize]="RowHeight" bitScrollLayout class="tw-pb-8">
|
||||
<bit-table [dataSource]="dataSource" layout="fixed">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
|
||||
import { TableModule } from "@bitwarden/components";
|
||||
import { ScrollLayoutDirective, TableModule } from "@bitwarden/components";
|
||||
|
||||
import { CollectionNameBadgeComponent } from "../../../admin-console/organizations/collections";
|
||||
import { GroupBadgeModule } from "../../../admin-console/organizations/collections/group-badge/group-badge.module";
|
||||
@@ -26,6 +26,7 @@ import { VaultItemsComponent } from "./vault-items.component";
|
||||
CollectionNameBadgeComponent,
|
||||
GroupBadgeModule,
|
||||
PipesModule,
|
||||
ScrollLayoutDirective,
|
||||
],
|
||||
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
|
||||
exports: [VaultItemsComponent],
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
// @ts-strict-ignore
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import {
|
||||
applicationConfig,
|
||||
componentWrapperDecorator,
|
||||
Meta,
|
||||
moduleMetadata,
|
||||
StoryObj,
|
||||
} from "@storybook/angular";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import {
|
||||
@@ -29,7 +35,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { LayoutComponent } from "@bitwarden/components";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||
@@ -49,8 +56,9 @@ export default {
|
||||
title: "Web/Vault/Items",
|
||||
component: VaultItemsComponent,
|
||||
decorators: [
|
||||
componentWrapperDecorator((story) => `<bit-layout>${story}</bit-layout>`),
|
||||
moduleMetadata({
|
||||
imports: [VaultItemsModule, RouterModule],
|
||||
imports: [VaultItemsModule, RouterModule, LayoutComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
|
||||
@@ -22,8 +22,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import { TrialFlowService } from "../../../../billing/services/trial-flow.service";
|
||||
import { VaultFilterService } from "../services/abstractions/vault-filter.service";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { RestrictedCipherType } from "@bitwarden/vault";
|
||||
import { RestrictedCipherType } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
|
||||
import { createFilterFunction } from "./filter-function";
|
||||
import { All } from "./routed-vault-filter.model";
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { RestrictedCipherType } from "@bitwarden/vault";
|
||||
import {
|
||||
isCipherViewRestricted,
|
||||
RestrictedCipherType,
|
||||
} from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
|
||||
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
|
||||
|
||||
@@ -83,24 +86,9 @@ export function createFilterFunction(
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Restricted types
|
||||
if (restrictedTypes && restrictedTypes.length > 0) {
|
||||
// Filter the cipher if that type is restricted unless
|
||||
// - The cipher belongs to an organization and that organization allows viewing the cipher type
|
||||
// OR
|
||||
// - The cipher belongs to the user's personal vault and at least one other organization does not restrict that type
|
||||
if (
|
||||
restrictedTypes.some(
|
||||
(restrictedType) =>
|
||||
restrictedType.cipherType === cipher.type &&
|
||||
(cipher.organizationId
|
||||
? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
|
||||
: restrictedType.allowViewOrgIds.length === 0),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (restrictedTypes && isCipherViewRestricted(cipher, restrictedTypes)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -18,13 +18,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import {
|
||||
BreadcrumbsModule,
|
||||
DialogService,
|
||||
MenuModule,
|
||||
SimpleDialogOptions,
|
||||
} from "@bitwarden/components";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/vault";
|
||||
|
||||
import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog";
|
||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
|
||||
@@ -66,6 +66,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||
import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
@@ -79,7 +80,6 @@ import {
|
||||
DecryptionFailureDialogComponent,
|
||||
DefaultCipherFormConfigService,
|
||||
PasswordRepromptService,
|
||||
RestrictedItemTypesService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
BrowserExtensionPromptService,
|
||||
|
||||
@@ -2,11 +2,11 @@ import { DestroyRef, Injectable } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { BehaviorSubject, fromEvent } from "rxjs";
|
||||
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||
import { AnonLayoutWrapperDataService } from "@bitwarden/components";
|
||||
|
||||
export const BrowserPromptState = {
|
||||
Loading: "loading",
|
||||
|
||||
@@ -7615,9 +7615,9 @@
|
||||
"message": "Service account updated",
|
||||
"description": "Notifies that a service account has been updated"
|
||||
},
|
||||
"newSaSelectAccess": {
|
||||
"message": "Type or select projects or secrets",
|
||||
"description": "Instructions for selecting projects or secrets for a new service account"
|
||||
"typeOrSelectProjects": {
|
||||
"message": "Type or select projects",
|
||||
"description": "Instructions for selecting projects for a service account"
|
||||
},
|
||||
"newSaTypeToFilter": {
|
||||
"message": "Type to filter",
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
|
||||
import { BitwardenLogo } from "@bitwarden/auth/angular";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProviderUserAcceptRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-accept.request";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Icons } from "@bitwarden/components";
|
||||
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
|
||||
|
||||
@Component({
|
||||
@@ -17,7 +17,7 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept
|
||||
standalone: false,
|
||||
})
|
||||
export class AcceptProviderComponent extends BaseAcceptComponent {
|
||||
protected logo = BitwardenLogo;
|
||||
protected logo = Icons.BitwardenLogo;
|
||||
providerName: string;
|
||||
providerId: string;
|
||||
providerUserId: string;
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
>
|
||||
{{ "providerUsersNeedConfirmed" | i18n }}
|
||||
</bit-callout>
|
||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { authGuard } from "@bitwarden/angular/auth/guards";
|
||||
import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AnonLayoutWrapperComponent } from "@bitwarden/components";
|
||||
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component";
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NgModule } from "@angular/core";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CardComponent, SearchModule } from "@bitwarden/components";
|
||||
import { CardComponent, ScrollLayoutDirective, SearchModule } from "@bitwarden/components";
|
||||
import { DangerZoneComponent } from "@bitwarden/web-vault/app/auth/settings/account/danger-zone.component";
|
||||
import { OrganizationPlansComponent } from "@bitwarden/web-vault/app/billing";
|
||||
import { PaymentComponent } from "@bitwarden/web-vault/app/billing/shared/payment/payment.component";
|
||||
@@ -53,6 +53,7 @@ import { VerifyRecoverDeleteProviderComponent } from "./verify-recover-delete-pr
|
||||
ScrollingModule,
|
||||
VerifyBankAccountComponent,
|
||||
CardComponent,
|
||||
ScrollLayoutDirective,
|
||||
PaymentComponent,
|
||||
],
|
||||
declarations: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Params } from "@angular/router";
|
||||
|
||||
import { BitwardenLogo } from "@bitwarden/auth/angular";
|
||||
import { Icons } from "@bitwarden/components";
|
||||
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
|
||||
|
||||
@Component({
|
||||
@@ -10,7 +10,7 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept
|
||||
standalone: false,
|
||||
})
|
||||
export class SetupProviderComponent extends BaseAcceptComponent {
|
||||
protected logo = BitwardenLogo;
|
||||
protected logo = Icons.BitwardenLogo;
|
||||
failedShortMessage = "inviteAcceptFailedShort";
|
||||
failedMessage = "inviteAcceptFailed";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { unauthGuardFn } from "@bitwarden/angular/auth/guards";
|
||||
import { AnonLayoutWrapperComponent } from "@bitwarden/auth/angular";
|
||||
import { AnonLayoutWrapperComponent } from "@bitwarden/components";
|
||||
import { deepLinkGuard } from "@bitwarden/web-vault/app/auth/guards/deep-link/deep-link.guard";
|
||||
import { RouteDataProperties } from "@bitwarden/web-vault/app/core";
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BasePortalOutlet } from "@angular/cdk/portal";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
@@ -33,8 +32,7 @@ export const openCreateClientDialog = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<
|
||||
CreateClientDialogParams,
|
||||
DialogRef<CreateClientDialogResultType, unknown>,
|
||||
BasePortalOutlet
|
||||
DialogRef<CreateClientDialogResultType, unknown>
|
||||
>,
|
||||
) =>
|
||||
dialogService.open<CreateClientDialogResultType, CreateClientDialogParams>(
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { filter, map, switchMap } from "rxjs/operators";
|
||||
|
||||
import { BitwardenLogo } from "@bitwarden/auth/angular";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -13,6 +12,7 @@ import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { ProviderKey } from "@bitwarden/common/types/key";
|
||||
import { Icons } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { BillingNotificationService } from "@bitwarden/web-vault/app/billing/services/billing-notification.service";
|
||||
import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept.component";
|
||||
@@ -22,7 +22,7 @@ import { BaseAcceptComponent } from "@bitwarden/web-vault/app/common/base.accept
|
||||
standalone: false,
|
||||
})
|
||||
export class SetupBusinessUnitComponent extends BaseAcceptComponent {
|
||||
protected bitwardenLogo = BitwardenLogo;
|
||||
protected bitwardenLogo = Icons.BitwardenLogo;
|
||||
|
||||
failedMessage = "emergencyInviteAcceptFailed";
|
||||
failedShortMessage = "emergencyInviteAcceptFailedShort";
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</ng-container>
|
||||
<bit-table-scroll *ngIf="!(isLoading$ | async)" [dataSource]="dataSource" [rowSize]="53">
|
||||
<ng-container header>
|
||||
<th bitCell bitSortable="name" default>{{ "members" | i18n }}</th>
|
||||
<th bitCell bitSortable="email" default>{{ "members" | i18n }}</th>
|
||||
<th bitCell bitSortable="groupsCount" class="tw-w-[278px]">{{ "groups" | i18n }}</th>
|
||||
<th bitCell bitSortable="collectionsCount" class="tw-w-[278px]">{{ "collections" | i18n }}</th>
|
||||
<th bitCell bitSortable="itemsCount" class="tw-w-[278px]">{{ "items" | i18n }}</th>
|
||||
|
||||
@@ -2,7 +2,15 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { Guid } from "@bitwarden/common/types/guid";
|
||||
|
||||
export class MemberAccessDetails extends BaseResponse {
|
||||
export class MemberAccessResponse extends BaseResponse {
|
||||
userName: string;
|
||||
email: string;
|
||||
twoFactorEnabled: boolean;
|
||||
accountRecoveryEnabled: boolean;
|
||||
userGuid: Guid;
|
||||
usesKeyConnector: boolean;
|
||||
|
||||
cipherIds: Guid[] = [];
|
||||
collectionId: string;
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
@@ -14,6 +22,14 @@ export class MemberAccessDetails extends BaseResponse {
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.userName = this.getResponseProperty("UserName");
|
||||
this.email = this.getResponseProperty("Email");
|
||||
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
||||
this.accountRecoveryEnabled = this.getResponseProperty("AccountRecoveryEnabled");
|
||||
this.userGuid = this.getResponseProperty("UserGuid");
|
||||
this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector");
|
||||
|
||||
this.cipherIds = this.getResponseProperty("CipherIds") || [];
|
||||
this.groupId = this.getResponseProperty("GroupId");
|
||||
this.collectionId = this.getResponseProperty("CollectionId");
|
||||
this.groupName = this.getResponseProperty("GroupName");
|
||||
@@ -24,34 +40,3 @@ export class MemberAccessDetails extends BaseResponse {
|
||||
this.manage = this.getResponseProperty("Manage");
|
||||
}
|
||||
}
|
||||
|
||||
export class MemberAccessResponse extends BaseResponse {
|
||||
userName: string;
|
||||
email: string;
|
||||
twoFactorEnabled: boolean;
|
||||
accountRecoveryEnabled: boolean;
|
||||
collectionsCount: number;
|
||||
groupsCount: number;
|
||||
totalItemCount: number;
|
||||
accessDetails: MemberAccessDetails[] = [];
|
||||
userGuid: Guid;
|
||||
usesKeyConnector: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.userName = this.getResponseProperty("UserName");
|
||||
this.email = this.getResponseProperty("Email");
|
||||
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
||||
this.accountRecoveryEnabled = this.getResponseProperty("AccountRecoveryEnabled");
|
||||
this.collectionsCount = this.getResponseProperty("CollectionsCount");
|
||||
this.groupsCount = this.getResponseProperty("GroupsCount");
|
||||
this.totalItemCount = this.getResponseProperty("TotalItemCount");
|
||||
this.userGuid = this.getResponseProperty("UserGuid");
|
||||
this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector");
|
||||
|
||||
const details = this.getResponseProperty("AccessDetails");
|
||||
if (details != null) {
|
||||
this.accessDetails = details.map((o: any) => new MemberAccessDetails(o));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { Guid } from "@bitwarden/common/types/guid";
|
||||
|
||||
import {
|
||||
MemberAccessDetails,
|
||||
MemberAccessResponse,
|
||||
} from "../response/member-access-report.response";
|
||||
import { MemberAccessResponse } from "../response/member-access-report.response";
|
||||
|
||||
export const memberAccessReportsMock: MemberAccessResponse[] = [
|
||||
{
|
||||
@@ -11,223 +9,290 @@ export const memberAccessReportsMock: MemberAccessResponse[] = [
|
||||
email: "sjohnson@email.com",
|
||||
twoFactorEnabled: true,
|
||||
accountRecoveryEnabled: true,
|
||||
groupsCount: 2,
|
||||
collectionsCount: 4,
|
||||
totalItemCount: 20,
|
||||
userGuid: "1234",
|
||||
userGuid: "1001" as Guid,
|
||||
usesKeyConnector: false,
|
||||
accessDetails: [
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c1",
|
||||
collectionName: new EncString("Collection 1"),
|
||||
groupName: "",
|
||||
itemCount: 10,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c2",
|
||||
collectionName: new EncString("Collection 2"),
|
||||
groupName: "",
|
||||
itemCount: 20,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c3",
|
||||
collectionName: new EncString("Collection 3"),
|
||||
groupName: "",
|
||||
itemCount: 30,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "g1",
|
||||
collectionId: "c1",
|
||||
collectionName: new EncString("Collection 1"),
|
||||
groupName: "Group 1",
|
||||
itemCount: 30,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "g1",
|
||||
collectionId: "c2",
|
||||
collectionName: new EncString("Collection 2"),
|
||||
groupName: "Group 1",
|
||||
itemCount: 20,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
],
|
||||
} as MemberAccessResponse,
|
||||
groupId: "",
|
||||
collectionId: "c1",
|
||||
collectionName: new EncString("Collection 1"),
|
||||
groupName: "",
|
||||
itemCount: 10,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Sarah Johnson",
|
||||
email: "sjohnson@email.com",
|
||||
twoFactorEnabled: true,
|
||||
accountRecoveryEnabled: true,
|
||||
userGuid: "1001" as Guid,
|
||||
usesKeyConnector: false,
|
||||
groupId: "",
|
||||
collectionId: "c2",
|
||||
collectionName: new EncString("Collection 2"),
|
||||
groupName: "",
|
||||
itemCount: 20,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Sarah Johnson",
|
||||
email: "sjohnson@email.com",
|
||||
twoFactorEnabled: true,
|
||||
accountRecoveryEnabled: true,
|
||||
userGuid: "1001" as Guid,
|
||||
usesKeyConnector: false,
|
||||
groupId: "",
|
||||
collectionId: "c3",
|
||||
collectionName: new EncString("Collection 3"),
|
||||
groupName: "",
|
||||
itemCount: 30,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Sarah Johnson",
|
||||
email: "sjohnson@email.com",
|
||||
twoFactorEnabled: true,
|
||||
accountRecoveryEnabled: true,
|
||||
userGuid: "1001",
|
||||
usesKeyConnector: false,
|
||||
groupId: "g1",
|
||||
collectionId: "c1",
|
||||
collectionName: new EncString("Collection 1"),
|
||||
groupName: "Group 1",
|
||||
itemCount: 30,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Sarah Johnson",
|
||||
email: "sjohnson@email.com",
|
||||
twoFactorEnabled: true,
|
||||
accountRecoveryEnabled: true,
|
||||
userGuid: "1001",
|
||||
usesKeyConnector: false,
|
||||
groupId: "g1",
|
||||
collectionId: "c2",
|
||||
collectionName: new EncString("Collection 2"),
|
||||
groupName: "Group 1",
|
||||
itemCount: 20,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "James Lull",
|
||||
email: "jlull@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
groupsCount: 2,
|
||||
collectionsCount: 4,
|
||||
totalItemCount: 20,
|
||||
userGuid: "1234",
|
||||
userGuid: "2001",
|
||||
usesKeyConnector: false,
|
||||
accessDetails: [
|
||||
{
|
||||
groupId: "g4",
|
||||
collectionId: "c4",
|
||||
groupName: "Group 4",
|
||||
collectionName: new EncString("Collection 4"),
|
||||
itemCount: 5,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "g4",
|
||||
collectionId: "c5",
|
||||
groupName: "Group 4",
|
||||
collectionName: new EncString("Collection 5"),
|
||||
itemCount: 15,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c4",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 4"),
|
||||
itemCount: 5,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c5",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 5"),
|
||||
itemCount: 15,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
],
|
||||
} as MemberAccessResponse,
|
||||
groupId: "g4",
|
||||
collectionId: "c4",
|
||||
groupName: "Group 4",
|
||||
collectionName: new EncString("Collection 4"),
|
||||
itemCount: 5,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "James Lull",
|
||||
email: "jlull@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
userGuid: "2001",
|
||||
usesKeyConnector: false,
|
||||
groupId: "g4",
|
||||
collectionId: "c5",
|
||||
groupName: "Group 4",
|
||||
collectionName: new EncString("Collection 5"),
|
||||
itemCount: 15,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "James Lull",
|
||||
email: "jlull@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
userGuid: "2001",
|
||||
usesKeyConnector: false,
|
||||
groupId: "",
|
||||
collectionId: "c4",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 4"),
|
||||
itemCount: 5,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "James Lull",
|
||||
email: "jlull@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
userGuid: "2001",
|
||||
usesKeyConnector: false,
|
||||
groupId: "",
|
||||
collectionId: "c5",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 5"),
|
||||
itemCount: 15,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Beth Williams",
|
||||
email: "bwilliams@email.com",
|
||||
twoFactorEnabled: true,
|
||||
accountRecoveryEnabled: true,
|
||||
groupsCount: 2,
|
||||
collectionsCount: 4,
|
||||
totalItemCount: 20,
|
||||
userGuid: "1234",
|
||||
userGuid: "3001",
|
||||
usesKeyConnector: false,
|
||||
accessDetails: [
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c6",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 6"),
|
||||
itemCount: 25,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "g6",
|
||||
collectionId: "c4",
|
||||
groupName: "Group 6",
|
||||
collectionName: new EncString("Collection 4"),
|
||||
itemCount: 35,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
],
|
||||
} as MemberAccessResponse,
|
||||
groupId: "",
|
||||
collectionId: "c6",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 6"),
|
||||
itemCount: 25,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Beth Williams",
|
||||
email: "bwilliams@email.com",
|
||||
twoFactorEnabled: true,
|
||||
accountRecoveryEnabled: true,
|
||||
userGuid: "3001",
|
||||
usesKeyConnector: false,
|
||||
groupId: "g6",
|
||||
collectionId: "c4",
|
||||
groupName: "Group 6",
|
||||
collectionName: new EncString("Collection 4"),
|
||||
itemCount: 35,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Ray Williams",
|
||||
email: "rwilliams@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
groupsCount: 2,
|
||||
collectionsCount: 4,
|
||||
totalItemCount: 20,
|
||||
userGuid: "1234",
|
||||
userGuid: "4000",
|
||||
usesKeyConnector: false,
|
||||
accessDetails: [
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c7",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 7"),
|
||||
itemCount: 8,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c8",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 8"),
|
||||
itemCount: 12,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c9",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 9"),
|
||||
itemCount: 16,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "g9",
|
||||
collectionId: "c7",
|
||||
groupName: "Group 9",
|
||||
collectionName: new EncString("Collection 7"),
|
||||
itemCount: 8,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "g10",
|
||||
collectionId: "c8",
|
||||
groupName: "Group 10",
|
||||
collectionName: new EncString("Collection 8"),
|
||||
itemCount: 12,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
{
|
||||
groupId: "g11",
|
||||
collectionId: "c9",
|
||||
groupName: "Group 11",
|
||||
collectionName: new EncString("Collection 9"),
|
||||
itemCount: 16,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
],
|
||||
} as MemberAccessResponse,
|
||||
groupId: "",
|
||||
collectionId: "c7",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 7"),
|
||||
itemCount: 8,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Ray Williams",
|
||||
email: "rwilliams@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
userGuid: "4000",
|
||||
usesKeyConnector: false,
|
||||
groupId: "",
|
||||
collectionId: "c8",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 8"),
|
||||
itemCount: 12,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Ray Williams",
|
||||
email: "rwilliams@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
userGuid: "4000",
|
||||
usesKeyConnector: false,
|
||||
groupId: "",
|
||||
collectionId: "c9",
|
||||
groupName: "",
|
||||
collectionName: new EncString("Collection 9"),
|
||||
itemCount: 16,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Ray Williams",
|
||||
email: "rwilliams@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
userGuid: "4000",
|
||||
usesKeyConnector: false,
|
||||
groupId: "g9",
|
||||
collectionId: "c7",
|
||||
groupName: "Group 9",
|
||||
collectionName: new EncString("Collection 7"),
|
||||
itemCount: 8,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Ray Williams",
|
||||
email: "rwilliams@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
userGuid: "4000",
|
||||
usesKeyConnector: false,
|
||||
groupId: "g10",
|
||||
collectionId: "c8",
|
||||
groupName: "Group 10",
|
||||
collectionName: new EncString("Collection 8"),
|
||||
itemCount: 12,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Ray Williams",
|
||||
email: "rwilliams@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
userGuid: "4000",
|
||||
usesKeyConnector: false,
|
||||
groupId: "g11",
|
||||
collectionId: "c9",
|
||||
groupName: "Group 11",
|
||||
collectionName: new EncString("Collection 9"),
|
||||
itemCount: 16,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
];
|
||||
|
||||
export const memberAccessWithoutAccessDetailsReportsMock: MemberAccessResponse[] = [
|
||||
@@ -236,34 +301,33 @@ export const memberAccessWithoutAccessDetailsReportsMock: MemberAccessResponse[]
|
||||
email: "asmith@email.com",
|
||||
twoFactorEnabled: true,
|
||||
accountRecoveryEnabled: true,
|
||||
groupsCount: 2,
|
||||
collectionsCount: 4,
|
||||
totalItemCount: 20,
|
||||
userGuid: "1234",
|
||||
userGuid: "1234" as Guid,
|
||||
usesKeyConnector: false,
|
||||
accessDetails: [
|
||||
{
|
||||
groupId: "",
|
||||
collectionId: "c1",
|
||||
collectionName: new EncString("Collection 1"),
|
||||
groupName: "Alice Group 1",
|
||||
itemCount: 10,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
} as MemberAccessDetails,
|
||||
],
|
||||
} as MemberAccessResponse,
|
||||
groupId: "",
|
||||
collectionId: "c1",
|
||||
collectionName: new EncString("Collection 1"),
|
||||
groupName: "Alice Group 1",
|
||||
itemCount: 10,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
{
|
||||
userName: "Robert Brown",
|
||||
email: "rbrown@email.com",
|
||||
twoFactorEnabled: false,
|
||||
accountRecoveryEnabled: false,
|
||||
groupsCount: 2,
|
||||
collectionsCount: 4,
|
||||
totalItemCount: 20,
|
||||
userGuid: "5678",
|
||||
userGuid: "5678" as Guid,
|
||||
usesKeyConnector: false,
|
||||
accessDetails: [] as MemberAccessDetails[],
|
||||
} as MemberAccessResponse,
|
||||
groupId: "",
|
||||
collectionId: "c1",
|
||||
collectionName: new EncString("Collection 1"),
|
||||
groupName: "",
|
||||
itemCount: 10,
|
||||
readOnly: false,
|
||||
hidePasswords: false,
|
||||
manage: false,
|
||||
cipherIds: [],
|
||||
} as unknown as MemberAccessResponse,
|
||||
];
|
||||
|
||||
@@ -35,36 +35,36 @@ describe("ImportService", () => {
|
||||
{
|
||||
name: "Sarah Johnson",
|
||||
email: "sjohnson@email.com",
|
||||
collectionsCount: 4,
|
||||
groupsCount: 2,
|
||||
itemsCount: 20,
|
||||
collectionsCount: 3,
|
||||
groupsCount: 1,
|
||||
itemsCount: 0,
|
||||
userGuid: expect.any(String),
|
||||
usesKeyConnector: expect.any(Boolean),
|
||||
},
|
||||
{
|
||||
name: "James Lull",
|
||||
email: "jlull@email.com",
|
||||
collectionsCount: 4,
|
||||
groupsCount: 2,
|
||||
itemsCount: 20,
|
||||
collectionsCount: 2,
|
||||
groupsCount: 1,
|
||||
itemsCount: 0,
|
||||
userGuid: expect.any(String),
|
||||
usesKeyConnector: expect.any(Boolean),
|
||||
},
|
||||
{
|
||||
name: "Beth Williams",
|
||||
email: "bwilliams@email.com",
|
||||
collectionsCount: 4,
|
||||
groupsCount: 2,
|
||||
itemsCount: 20,
|
||||
collectionsCount: 2,
|
||||
groupsCount: 1,
|
||||
itemsCount: 0,
|
||||
userGuid: expect.any(String),
|
||||
usesKeyConnector: expect.any(Boolean),
|
||||
},
|
||||
{
|
||||
name: "Ray Williams",
|
||||
email: "rwilliams@email.com",
|
||||
collectionsCount: 4,
|
||||
groupsCount: 2,
|
||||
itemsCount: 20,
|
||||
collectionsCount: 3,
|
||||
groupsCount: 3,
|
||||
itemsCount: 0,
|
||||
userGuid: expect.any(String),
|
||||
usesKeyConnector: expect.any(Boolean),
|
||||
},
|
||||
@@ -82,8 +82,8 @@ describe("ImportService", () => {
|
||||
(item) =>
|
||||
(item.name === "Sarah Johnson" &&
|
||||
item.group === "Group 1" &&
|
||||
item.totalItems === "20") ||
|
||||
(item.name === "James Lull" && item.group === "Group 4" && item.totalItems === "5"),
|
||||
item.totalItems === "0") ||
|
||||
(item.name === "James Lull" && item.group === "Group 4" && item.totalItems === "0"),
|
||||
)
|
||||
.map((item) => ({
|
||||
name: item.name,
|
||||
@@ -102,7 +102,7 @@ describe("ImportService", () => {
|
||||
twoStepLogin: "memberAccessReportTwoFactorEnabledTrue",
|
||||
accountRecovery: "memberAccessReportAuthenticationEnabledTrue",
|
||||
group: "Group 1",
|
||||
totalItems: "20",
|
||||
totalItems: "0",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
email: "jlull@email.com",
|
||||
@@ -110,7 +110,7 @@ describe("ImportService", () => {
|
||||
twoStepLogin: "memberAccessReportTwoFactorEnabledFalse",
|
||||
accountRecovery: "memberAccessReportAuthenticationEnabledFalse",
|
||||
group: "Group 4",
|
||||
totalItems: "5",
|
||||
totalItems: "0",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
@@ -131,7 +131,7 @@ describe("ImportService", () => {
|
||||
twoStepLogin: "memberAccessReportTwoFactorEnabledTrue",
|
||||
accountRecovery: "memberAccessReportAuthenticationEnabledTrue",
|
||||
group: "Alice Group 1",
|
||||
totalItems: "10",
|
||||
totalItems: "0",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
email: "rbrown@email.com",
|
||||
|
||||
@@ -5,13 +5,13 @@ import { Injectable } from "@angular/core";
|
||||
import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { Guid, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
getPermissionList,
|
||||
convertToPermission,
|
||||
} from "@bitwarden/web-vault/app/admin-console/organizations/shared/components/access-selector";
|
||||
|
||||
import { MemberAccessDetails } from "../response/member-access-report.response";
|
||||
import { MemberAccessResponse } from "../response/member-access-report.response";
|
||||
import { MemberAccessExportItem } from "../view/member-access-export.view";
|
||||
import { MemberAccessReportView } from "../view/member-access-report.view";
|
||||
|
||||
@@ -34,15 +34,44 @@ export class MemberAccessReportService {
|
||||
organizationId: OrganizationId,
|
||||
): Promise<MemberAccessReportView[]> {
|
||||
const memberAccessData = await this.reportApiService.getMemberAccessData(organizationId);
|
||||
const memberAccessReportViewCollection = memberAccessData.map((userData) => ({
|
||||
name: userData.userName,
|
||||
email: userData.email,
|
||||
collectionsCount: userData.collectionsCount,
|
||||
groupsCount: userData.groupsCount,
|
||||
itemsCount: userData.totalItemCount,
|
||||
userGuid: userData.userGuid,
|
||||
usesKeyConnector: userData.usesKeyConnector,
|
||||
}));
|
||||
|
||||
// group member access data by userGuid
|
||||
const userMap = new Map<Guid, MemberAccessResponse[]>();
|
||||
memberAccessData.forEach((userData) => {
|
||||
const userGuid = userData.userGuid;
|
||||
if (!userMap.has(userGuid)) {
|
||||
userMap.set(userGuid, []);
|
||||
}
|
||||
userMap.get(userGuid)?.push(userData);
|
||||
});
|
||||
|
||||
// aggregate user data
|
||||
const memberAccessReportViewCollection: MemberAccessReportView[] = [];
|
||||
userMap.forEach((userDataArray, userGuid) => {
|
||||
const collectionCount = this.getDistinctCount<string>(
|
||||
userDataArray.map((data) => data.collectionId).filter((id) => !!id),
|
||||
);
|
||||
const groupCount = this.getDistinctCount<string>(
|
||||
userDataArray.map((data) => data.groupId).filter((id) => !!id),
|
||||
);
|
||||
const itemsCount = this.getDistinctCount<Guid>(
|
||||
userDataArray
|
||||
.flatMap((data) => data.cipherIds)
|
||||
.filter((id) => id !== "00000000-0000-0000-0000-000000000000"),
|
||||
);
|
||||
const aggregatedData = {
|
||||
userGuid: userGuid,
|
||||
name: userDataArray[0].userName,
|
||||
email: userDataArray[0].email,
|
||||
collectionsCount: collectionCount,
|
||||
groupsCount: groupCount,
|
||||
itemsCount: itemsCount,
|
||||
usesKeyConnector: userDataArray.some((data) => data.usesKeyConnector),
|
||||
};
|
||||
|
||||
memberAccessReportViewCollection.push(aggregatedData);
|
||||
});
|
||||
|
||||
return memberAccessReportViewCollection;
|
||||
}
|
||||
|
||||
@@ -50,13 +79,8 @@ export class MemberAccessReportService {
|
||||
organizationId: OrganizationId,
|
||||
): Promise<MemberAccessExportItem[]> {
|
||||
const memberAccessReports = await this.reportApiService.getMemberAccessData(organizationId);
|
||||
const collectionNames = memberAccessReports.flatMap((item) =>
|
||||
item.accessDetails.map((dtl) => {
|
||||
if (dtl.collectionName) {
|
||||
return dtl.collectionName.encryptedString;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const collectionNames = memberAccessReports.map((item) => item.collectionName.encryptedString);
|
||||
|
||||
const collectionNameMap = new Map(collectionNames.map((col) => [col, ""]));
|
||||
for await (const key of collectionNameMap.keys()) {
|
||||
const decrypted = new EncString(key);
|
||||
@@ -64,56 +88,35 @@ export class MemberAccessReportService {
|
||||
collectionNameMap.set(key, decrypted.decryptedValue);
|
||||
}
|
||||
|
||||
const exportItems = memberAccessReports.flatMap((report) => {
|
||||
// to include users without access details
|
||||
// which means a user has no groups, collections or items
|
||||
if (report.accessDetails.length === 0) {
|
||||
return [
|
||||
{
|
||||
email: report.email,
|
||||
name: report.userName,
|
||||
twoStepLogin: report.twoFactorEnabled
|
||||
? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue")
|
||||
: this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"),
|
||||
accountRecovery: report.accountRecoveryEnabled
|
||||
? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue")
|
||||
: this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"),
|
||||
group: this.i18nService.t("memberAccessReportNoGroup"),
|
||||
collection: this.i18nService.t("memberAccessReportNoCollection"),
|
||||
collectionPermission: this.i18nService.t("memberAccessReportNoCollectionPermission"),
|
||||
totalItems: "0",
|
||||
},
|
||||
];
|
||||
}
|
||||
const userDetails = report.accessDetails.map((detail) => {
|
||||
const collectionName = collectionNameMap.get(detail.collectionName.encryptedString);
|
||||
return {
|
||||
email: report.email,
|
||||
name: report.userName,
|
||||
twoStepLogin: report.twoFactorEnabled
|
||||
? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue")
|
||||
: this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"),
|
||||
accountRecovery: report.accountRecoveryEnabled
|
||||
? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue")
|
||||
: this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"),
|
||||
group: detail.groupName
|
||||
? detail.groupName
|
||||
: this.i18nService.t("memberAccessReportNoGroup"),
|
||||
collection: collectionName
|
||||
? collectionName
|
||||
: this.i18nService.t("memberAccessReportNoCollection"),
|
||||
collectionPermission: detail.collectionId
|
||||
? this.getPermissionText(detail)
|
||||
: this.i18nService.t("memberAccessReportNoCollectionPermission"),
|
||||
totalItems: detail.itemCount.toString(),
|
||||
};
|
||||
});
|
||||
return userDetails;
|
||||
const exportItems = memberAccessReports.map((report) => {
|
||||
const collectionName = collectionNameMap.get(report.collectionName.encryptedString);
|
||||
return {
|
||||
email: report.email,
|
||||
name: report.userName,
|
||||
twoStepLogin: report.twoFactorEnabled
|
||||
? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue")
|
||||
: this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"),
|
||||
accountRecovery: report.accountRecoveryEnabled
|
||||
? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue")
|
||||
: this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"),
|
||||
group: report.groupName
|
||||
? report.groupName
|
||||
: this.i18nService.t("memberAccessReportNoGroup"),
|
||||
collection: collectionName
|
||||
? collectionName
|
||||
: this.i18nService.t("memberAccessReportNoCollection"),
|
||||
collectionPermission: report.collectionId
|
||||
? this.getPermissionText(report)
|
||||
: this.i18nService.t("memberAccessReportNoCollectionPermission"),
|
||||
totalItems: report.cipherIds
|
||||
.filter((_) => _ != "00000000-0000-0000-0000-000000000000")
|
||||
.length.toString(),
|
||||
};
|
||||
});
|
||||
return exportItems.flat();
|
||||
}
|
||||
|
||||
private getPermissionText(accessDetails: MemberAccessDetails): string {
|
||||
private getPermissionText(accessDetails: MemberAccessResponse): string {
|
||||
const permissionList = getPermissionList();
|
||||
const collectionSelectionView = new CollectionAccessSelectionView({
|
||||
id: accessDetails.groupId ?? accessDetails.collectionId,
|
||||
@@ -125,4 +128,9 @@ export class MemberAccessReportService {
|
||||
permissionList.find((p) => p.perm === convertToPermission(collectionSelectionView))?.labelId,
|
||||
);
|
||||
}
|
||||
|
||||
private getDistinctCount<T>(items: T[]): number {
|
||||
const uniqueItems = new Set(items);
|
||||
return uniqueItems.size;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
|
||||
<div class="tw-w-2/5">
|
||||
<div class="tw-w-full tw-max-w-[1200px]">
|
||||
<p class="tw-mt-8" *ngIf="!loading">
|
||||
{{ "projectPeopleDescription" | i18n }}
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
|
||||
<div class="tw-w-2/5">
|
||||
<div class="tw-w-full tw-max-w-[1200px]">
|
||||
<p class="tw-mt-8" *ngIf="!loading">
|
||||
{{ "projectMachineAccountsDescription" | i18n }}
|
||||
</p>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogRef, DIALOG_DATA, BitValidators, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { ServiceAccountView } from "../../models/view/service-account.view";
|
||||
@@ -46,8 +46,8 @@ export class ServiceAccountDialogComponent implements OnInit {
|
||||
@Inject(DIALOG_DATA) private data: ServiceAccountOperation,
|
||||
private serviceAccountService: ServiceAccountService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private toastService: ToastService,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -87,8 +87,17 @@ export class ServiceAccountDialogComponent implements OnInit {
|
||||
let serviceAccountMessage: string;
|
||||
|
||||
if (this.data.operation == OperationType.Add) {
|
||||
await this.serviceAccountService.create(this.data.organizationId, serviceAccountView);
|
||||
const newServiceAccount = await this.serviceAccountService.create(
|
||||
this.data.organizationId,
|
||||
serviceAccountView,
|
||||
);
|
||||
serviceAccountMessage = this.i18nService.t("machineAccountCreated");
|
||||
await this.router.navigate([
|
||||
"sm",
|
||||
this.data.organizationId,
|
||||
"machine-accounts",
|
||||
newServiceAccount.id,
|
||||
]);
|
||||
} else {
|
||||
await this.serviceAccountService.update(
|
||||
this.data.serviceAccountId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user