1
0
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:
Patrick-Pimentel-Bitwarden
2025-06-19 13:02:39 -04:00
committed by GitHub
183 changed files with 2777 additions and 1644 deletions

3
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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."
}
}

View File

@@ -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";

View File

@@ -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

View File

@@ -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);

View File

@@ -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.

View File

@@ -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";

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);
};

View File

@@ -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",
},

View File

@@ -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,

View File

@@ -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,

View File

@@ -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";

View File

@@ -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,

View File

@@ -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,

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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}`,

View File

@@ -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",
},

View File

@@ -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: [

View File

@@ -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>;
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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> {

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
});
});
});

View File

@@ -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;
}
}
}

View File

@@ -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>;
}

View File

@@ -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();
}

View File

@@ -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,
);
});

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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"
}
}
}
}
}

View File

@@ -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(

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>

View File

@@ -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,
);
}
}

View File

@@ -0,0 +1 @@
export * from "./assign-collections-desktop.component";

View File

@@ -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()"

View File

@@ -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() {

View File

@@ -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>&nbsp;{{ "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>&nbsp;{{ "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>&nbsp;{{ "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>&nbsp;{{
"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>&nbsp;{{ "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>&nbsp;{{
typeFilter.labelKey | i18n
}}
</button>
</span>
</li>
}
</ul>

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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())

View File

@@ -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) => {

View File

@@ -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">

View File

@@ -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 () => {

View File

@@ -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";

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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",

View File

@@ -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],
})

View File

@@ -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>

View File

@@ -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: [],

View File

@@ -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: [],
}),

View File

@@ -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);
}

View File

@@ -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";

View File

@@ -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();

View File

@@ -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,

View File

@@ -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";

View File

@@ -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,
},

View File

@@ -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";

View File

@@ -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>

View File

@@ -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],

View File

@@ -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,

View File

@@ -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";

View File

@@ -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";

View File

@@ -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;
};

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;

View File

@@ -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>

View File

@@ -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";

View File

@@ -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: [

View File

@@ -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";

View File

@@ -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";

View File

@@ -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>(

View File

@@ -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";

View File

@@ -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>

View File

@@ -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));
}
}
}

View File

@@ -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,
];

View File

@@ -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",

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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