mirror of
https://github.com/bitwarden/browser
synced 2026-02-17 09:59:41 +00:00
Merge remote-tracking branch 'origin' into innovation/archive/web-work
This commit is contained in:
@@ -1,18 +1,83 @@
|
||||
FROM ghcr.io/bitwarden/server
|
||||
###############################################
|
||||
# Build stage 1 #
|
||||
###############################################
|
||||
ARG NODE_VERSION=20
|
||||
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS node-build
|
||||
|
||||
ARG NPM_COMMAND=dist:bit:selfhost
|
||||
|
||||
WORKDIR /source
|
||||
COPY . .
|
||||
|
||||
RUN npm ci
|
||||
|
||||
WORKDIR /source/apps/web
|
||||
RUN npm run ${NPM_COMMAND}
|
||||
|
||||
###############################################
|
||||
# Build stage 2 #
|
||||
###############################################
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
|
||||
# Docker buildx supplies the value for this arg
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Determine proper runtime value for .NET
|
||||
# We put the value in a file to be read by later layers.
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
RID=linux-x64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
RID=linux-arm64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||
RID=linux-arm ; \
|
||||
fi \
|
||||
&& echo "RID=$RID" > /tmp/rid.txt
|
||||
|
||||
# Copy csproj files as distinct layers
|
||||
WORKDIR /source
|
||||
COPY server/util/Server/*.csproj ./util/Server/
|
||||
COPY server/Directory.Build.props .
|
||||
COPY server/.editorconfig .
|
||||
|
||||
# Restore Server project dependencies and tools
|
||||
WORKDIR /source/util/Server
|
||||
RUN . /tmp/rid.txt && dotnet restore -r $RID
|
||||
|
||||
# Copy required project files
|
||||
WORKDIR /source
|
||||
COPY server/util/Server/. ./util/Server/
|
||||
COPY server/.git/. ./.git/
|
||||
|
||||
# Build Server app
|
||||
WORKDIR /source/util/Server
|
||||
RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Server --no-restore --no-self-contained -r $RID
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
###############################################
|
||||
# App stage #
|
||||
###############################################
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
EXPOSE 5000
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy app from the build stage
|
||||
WORKDIR /bitwarden_server
|
||||
COPY --from=build /app/Server ./
|
||||
|
||||
ENV ASPNETCORE_URLS http://+:5000
|
||||
WORKDIR /app
|
||||
EXPOSE 5000
|
||||
COPY ./build .
|
||||
COPY entrypoint.sh /
|
||||
COPY --from=node-build /source/apps/web/build .
|
||||
COPY --from=node-build /source/apps/web/entrypoint.sh /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2025.3.0",
|
||||
"version": "2025.4.0",
|
||||
"scripts": {
|
||||
"build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
"build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||
@@ -18,7 +17,13 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { GroupApiService, GroupView } from "../../core";
|
||||
|
||||
@@ -5,6 +5,7 @@ import { firstValueFrom, Subject } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -48,6 +49,7 @@ export class VaultFilterComponent
|
||||
protected billingApiService: BillingApiServiceAbstraction,
|
||||
protected dialogService: DialogService,
|
||||
protected configService: ConfigService,
|
||||
protected accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
vaultFilterService,
|
||||
@@ -58,6 +60,7 @@ export class VaultFilterComponent
|
||||
billingApiService,
|
||||
dialogService,
|
||||
configService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,8 +89,8 @@ export class VaultFilterComponent
|
||||
const collapsedNodes = await firstValueFrom(this.vaultFilterService.collapsedFilterNodes$);
|
||||
|
||||
collapsedNodes.delete("AllCollections");
|
||||
|
||||
await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes);
|
||||
const userId = await firstValueFrom(this.activeUserId$);
|
||||
await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes, userId);
|
||||
}
|
||||
|
||||
protected async addCollectionFilter(): Promise<VaultFilterSection> {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
<div [class]="hideVaultFilters ? 'tw-w-4/5' : 'tw-w-3/4'">
|
||||
<div [class]="hideVaultFilters ? 'tw-w-full' : 'tw-w-3/4'">
|
||||
<bit-toggle-group
|
||||
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||
[selected]="addAccessStatus$ | async"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import {
|
||||
@@ -45,7 +44,6 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -63,6 +61,7 @@ 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 {
|
||||
DialogRef,
|
||||
BannerModule,
|
||||
DialogService,
|
||||
Icons,
|
||||
@@ -196,7 +195,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private refresh$ = new BehaviorSubject<void>(null);
|
||||
private destroy$ = new Subject<void>();
|
||||
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
|
||||
private resellerManagedOrgAlert: boolean;
|
||||
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
|
||||
|
||||
private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe(
|
||||
@@ -264,10 +262,6 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.resellerManagedOrgAlert = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.ResellerManagedOrgAlert,
|
||||
);
|
||||
|
||||
this.trashCleanupWarning = this.i18nService.t(
|
||||
this.platformUtilsService.isSelfHost()
|
||||
? "trashCleanupWarningSelfHosted"
|
||||
@@ -654,7 +648,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
this.resellerWarning$ = organization$.pipe(
|
||||
filter((org) => org.isOwner && this.resellerManagedOrgAlert),
|
||||
filter((org) => org.isOwner),
|
||||
switchMap((org) =>
|
||||
from(this.billingApiService.getOrganizationBillingMetadata(org.id)).pipe(
|
||||
map((metadata) => ({ org, metadata })),
|
||||
@@ -1232,6 +1226,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
organizationId: this.organization?.id,
|
||||
parentCollectionId: this.selectedCollection?.node.id,
|
||||
limitNestedCollections: !this.organization.canEditAnyCollection,
|
||||
isAdminConsoleActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1257,6 +1252,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
readonly: readonly,
|
||||
isAddAccessCollection: c.unmanaged,
|
||||
limitNestedCollections: !this.organization.canEditAnyCollection,
|
||||
isAdminConsoleActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export class OrganizationUserAdminView {
|
||||
type: OrganizationUserType;
|
||||
status: OrganizationUserStatusType;
|
||||
externalId: string;
|
||||
ssoExternalId: string;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
hasMasterPassword: boolean;
|
||||
@@ -39,6 +40,7 @@ export class OrganizationUserAdminView {
|
||||
view.type = response.type;
|
||||
view.status = response.status;
|
||||
view.externalId = response.externalId;
|
||||
view.ssoExternalId = response.ssoExternalId;
|
||||
view.permissions = response.permissions;
|
||||
view.resetPasswordEnrolled = response.resetPasswordEnrolled;
|
||||
view.collections = response.collections.map((c) => ({
|
||||
|
||||
@@ -59,7 +59,7 @@ export function isEnterpriseOrgGuard(showError: boolean = true): CanActivateFn {
|
||||
content: { key: "onlyAvailableForEnterpriseOrganization" },
|
||||
acceptButtonText: { key: "upgradeOrganization" },
|
||||
type: "info",
|
||||
icon: "bwi-arrow-circle-up",
|
||||
icon: "bwi-plus-circle",
|
||||
});
|
||||
if (upgradeConfirmed) {
|
||||
await router.navigate(["organizations", org.id, "billing", "subscription"], {
|
||||
|
||||
@@ -58,7 +58,7 @@ export function isPaidOrgGuard(): CanActivateFn {
|
||||
content: { key: "upgradeOrganizationCloseSecurityGapsDesc" },
|
||||
acceptButtonText: { key: "upgradeOrganization" },
|
||||
type: "info",
|
||||
icon: "bwi-arrow-circle-up",
|
||||
icon: "bwi-plus-circle",
|
||||
});
|
||||
if (upgradeConfirmed) {
|
||||
await router.navigate(["organizations", org.id, "billing", "subscription"], {
|
||||
|
||||
@@ -100,20 +100,44 @@ describe("Organization Permissions Guard", () => {
|
||||
|
||||
it("permits navigation if the user has permissions", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockImplementation((_org) => true);
|
||||
permissionsCallback.mockReturnValue(true);
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(
|
||||
async () => await organizationPermissionsGuard(permissionsCallback)(route, state),
|
||||
);
|
||||
|
||||
expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId }));
|
||||
expect(permissionsCallback).toHaveBeenCalledTimes(1);
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("handles a Promise returned from the callback", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockReturnValue(Promise.resolve(true));
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(() =>
|
||||
organizationPermissionsGuard(permissionsCallback)(route, state),
|
||||
);
|
||||
|
||||
expect(permissionsCallback).toHaveBeenCalledTimes(1);
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
it("handles an Observable returned from the callback", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockReturnValue(of(true));
|
||||
|
||||
const actual = await TestBed.runInInjectionContext(() =>
|
||||
organizationPermissionsGuard(permissionsCallback)(route, state),
|
||||
);
|
||||
|
||||
expect(permissionsCallback).toHaveBeenCalledTimes(1);
|
||||
expect(actual).toBe(true);
|
||||
});
|
||||
|
||||
describe("if the user does not have permissions", () => {
|
||||
it("and there is no Item ID, block navigation", async () => {
|
||||
const permissionsCallback = jest.fn();
|
||||
permissionsCallback.mockImplementation((_org) => false);
|
||||
permissionsCallback.mockReturnValue(false);
|
||||
|
||||
state = mock<RouterStateSnapshot>({
|
||||
root: mock<ActivatedRouteSnapshot>({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { inject } from "@angular/core";
|
||||
import { EnvironmentInjector, inject, runInInjectionContext } from "@angular/core";
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivateFn,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from "@angular/router";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
import { firstValueFrom, isObservable, Observable, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
canAccessOrgAdmin,
|
||||
@@ -42,7 +42,9 @@ import { ToastService } from "@bitwarden/components";
|
||||
* proceeds as expected.
|
||||
*/
|
||||
export function organizationPermissionsGuard(
|
||||
permissionsCallback?: (organization: Organization) => boolean,
|
||||
permissionsCallback?: (
|
||||
organization: Organization,
|
||||
) => boolean | Promise<boolean> | Observable<boolean>,
|
||||
): CanActivateFn {
|
||||
return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
const router = inject(Router);
|
||||
@@ -51,6 +53,7 @@ export function organizationPermissionsGuard(
|
||||
const i18nService = inject(I18nService);
|
||||
const syncService = inject(SyncService);
|
||||
const accountService = inject(AccountService);
|
||||
const environmentInjector = inject(EnvironmentInjector);
|
||||
|
||||
// TODO: We need to fix issue once and for all.
|
||||
if ((await syncService.getLastSync()) == null) {
|
||||
@@ -78,7 +81,22 @@ export function organizationPermissionsGuard(
|
||||
return router.createUrlTree(["/"]);
|
||||
}
|
||||
|
||||
const hasPermissions = permissionsCallback == null || permissionsCallback(org);
|
||||
if (permissionsCallback == null) {
|
||||
// No additional permission checks required, allow navigation
|
||||
return true;
|
||||
}
|
||||
|
||||
const callbackResult = runInInjectionContext(environmentInjector, () =>
|
||||
permissionsCallback(org),
|
||||
);
|
||||
|
||||
const hasPermissions = isObservable(callbackResult)
|
||||
? await firstValueFrom(callbackResult) // handles observables
|
||||
: await Promise.resolve(callbackResult); // handles promises and boolean values
|
||||
|
||||
if (hasPermissions !== true && hasPermissions !== false) {
|
||||
throw new Error("Permission callback did not resolve to a boolean.");
|
||||
}
|
||||
|
||||
if (!hasPermissions) {
|
||||
// Handle linkable ciphers for organizations the user only has view access to
|
||||
|
||||
@@ -19,12 +19,26 @@
|
||||
*ngIf="canShowVaultTab(organization)"
|
||||
>
|
||||
</bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-user"
|
||||
[text]="'members' | i18n"
|
||||
route="members"
|
||||
*ngIf="canShowMembersTab(organization)"
|
||||
></bit-nav-item>
|
||||
|
||||
<ng-container *ngIf="canShowMembersTab(organization)">
|
||||
<ng-container *ngIf="showSponsoredFamiliesDropdown$ | async; else regularMembersItem">
|
||||
<bit-nav-group icon="bwi-user" [text]="'members' | i18n" route="members">
|
||||
<bit-nav-item
|
||||
[text]="'members' | i18n"
|
||||
route="members"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
[text]="'sponsoredFamilies' | i18n"
|
||||
route="members/sponsored-families"
|
||||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
</ng-container>
|
||||
<ng-template #regularMembersItem>
|
||||
<bit-nav-item icon="bwi-user" [text]="'members' | i18n" route="members"></bit-nav-item>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<bit-nav-item
|
||||
icon="bwi-users"
|
||||
[text]="'groups' | i18n"
|
||||
@@ -64,7 +78,7 @@
|
||||
</ng-container>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item
|
||||
icon="bwi-providers"
|
||||
icon="bwi-msp"
|
||||
[text]="'integrations' | i18n"
|
||||
route="integrations"
|
||||
*ngIf="integrationPageEnabled$ | async"
|
||||
@@ -83,7 +97,7 @@
|
||||
<bit-nav-item
|
||||
[text]="'policies' | i18n"
|
||||
route="settings/policies"
|
||||
*ngIf="organization.canManagePolicies"
|
||||
*ngIf="canShowPoliciesTab$ | async"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
[text]="'twoStepLogin' | i18n"
|
||||
|
||||
@@ -22,6 +22,7 @@ import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -29,6 +30,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { BannerModule, IconModule } from "@bitwarden/components";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../../../billing/services/free-families-policy.service";
|
||||
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
|
||||
import { WebLayoutModule } from "../../../layouts/web-layout.module";
|
||||
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
||||
@@ -66,6 +68,8 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
|
||||
showAccountDeprovisioningBanner$: Observable<boolean>;
|
||||
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
|
||||
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
|
||||
protected canShowPoliciesTab$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@@ -76,6 +80,8 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
private providerService: ProviderService,
|
||||
protected bannerService: AccountDeprovisioningBannerService,
|
||||
private accountService: AccountService,
|
||||
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -92,6 +98,8 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
),
|
||||
filter((org) => org != null),
|
||||
);
|
||||
this.showSponsoredFamiliesDropdown$ =
|
||||
this.freeFamiliesPolicyService.showSponsoredFamiliesDropdown$(this.organization$);
|
||||
|
||||
this.showAccountDeprovisioningBanner$ = combineLatest([
|
||||
this.bannerService.showBanner$,
|
||||
@@ -118,7 +126,10 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
),
|
||||
);
|
||||
|
||||
this.hideNewOrgButton$ = this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg);
|
||||
this.hideNewOrgButton$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.SingleOrg, userId)),
|
||||
);
|
||||
|
||||
const provider$ = this.organization$.pipe(
|
||||
switchMap((organization) => this.providerService.get$(organization.providerId)),
|
||||
@@ -140,6 +151,18 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
))
|
||||
? "claimedDomains"
|
||||
: "domainVerification";
|
||||
|
||||
this.canShowPoliciesTab$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService
|
||||
.isBreadcrumbingPoliciesEnabled$(organization)
|
||||
.pipe(
|
||||
map(
|
||||
(isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
canShowVaultTab(organization: Organization): boolean {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
@@ -15,7 +14,13 @@ import { EventView } from "@bitwarden/common/models/view/event.view";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogService,
|
||||
TableDataSource,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { EventService } from "../../../core";
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
@let usePlaceHolderEvents = !organization?.useEvents && (isBreadcrumbEventLogsEnabled$ | async);
|
||||
<app-header>
|
||||
<span bitBadge variant="primary" slot="title-suffix" *ngIf="usePlaceHolderEvents">
|
||||
<span
|
||||
bitBadge
|
||||
variant="primary"
|
||||
slot="title-suffix"
|
||||
class="tw-ml-2 tw-mt-1.5 tw-inline-flex tw-items-center"
|
||||
*ngIf="usePlaceHolderEvents"
|
||||
>
|
||||
{{ "upgrade" | i18n }}
|
||||
</span>
|
||||
</app-header>
|
||||
@@ -111,10 +117,10 @@
|
||||
|
||||
<ng-container *ngIf="loaded && usePlaceHolderEvents">
|
||||
<div
|
||||
class="tw-relative tw--top-72 tw-bg-[#ffffff] tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center"
|
||||
class="tw-relative tw--top-72 tw-bg-background tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center tw-h-[19rem]"
|
||||
>
|
||||
<div
|
||||
class="tw-bg-[#ffffff] tw-max-w-xl tw-flex-col tw-justify-center tw-text-center tw-p-5 tw-px-10 tw-rounded tw-border-0 tw-border-b tw-border-secondary-300 tw-border-solid tw-mt-5"
|
||||
class="tw-bg-background tw-max-w-xl tw-flex-col tw-justify-center tw-text-center tw-p-5 tw-px-10 tw-rounded tw-border-0 tw-border-b tw-border-secondary-300 tw-border-solid tw-mt-5"
|
||||
>
|
||||
<i class="bwi bwi-2x bwi-business tw-text-primary-600"></i>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<input bitInput appAutofocus type="text" formControlName="name" />
|
||||
<bit-hint>{{ "characterMaximum" | i18n: 100 }}</bit-hint>
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-form-field *ngIf="isExternalIdVisible$ | async">
|
||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="externalId" />
|
||||
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import {
|
||||
@@ -29,12 +28,20 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { InternalGroupApiService as GroupService } from "../core";
|
||||
import {
|
||||
@@ -215,6 +222,10 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
this.groupDetails$,
|
||||
]).pipe(map(([allowAdminAccess, groupDetails]) => !allowAdminAccess && groupDetails != null));
|
||||
|
||||
protected isExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(map((isEnabled) => !isEnabled || !!this.groupForm.get("externalId")?.value));
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
||||
private dialogRef: DialogRef<GroupAddEditDialogResultType>,
|
||||
@@ -231,6 +242,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
private accountService: AccountService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<form [formGroup]="confirmForm" [bitSubmit]="submit">
|
||||
<bit-dialog
|
||||
dialogSize="large"
|
||||
[loading]="loading"
|
||||
[title]="'trustOrganization' | i18n"
|
||||
[subtitle]="params.name"
|
||||
>
|
||||
<ng-container bitDialogContent>
|
||||
<bit-callout type="warning">{{ "orgTrustWarning" | i18n }}</bit-callout>
|
||||
<p bitTypography="body1">
|
||||
{{ "fingerprintPhrase" | i18n }} <code>{{ fingerprint }}</code>
|
||||
</p>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
<span>{{ "trust" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton bitFormButton buttonType="secondary" type="button" bitDialogClose>
|
||||
{{ "doNotTrust" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,69 @@
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit, Inject } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
type OrganizationTrustDialogData = {
|
||||
/** display name of the organization */
|
||||
name: string;
|
||||
/** identifies the organization */
|
||||
orgId: string;
|
||||
/** org public key */
|
||||
publicKey: Uint8Array;
|
||||
};
|
||||
@Component({
|
||||
selector: "organization-trust",
|
||||
templateUrl: "organization-trust.component.html",
|
||||
})
|
||||
export class OrganizationTrustComponent implements OnInit {
|
||||
loading = true;
|
||||
fingerprint: string = "";
|
||||
confirmForm = this.formBuilder.group({});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: OrganizationTrustDialogData,
|
||||
private formBuilder: FormBuilder,
|
||||
private keyService: KeyService,
|
||||
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||
private logService: LogService,
|
||||
private dialogRef: DialogRef<boolean>,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const fingerprint = await this.keyService.getFingerprint(
|
||||
this.params.orgId,
|
||||
this.params.publicKey,
|
||||
);
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join("-");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dialogRef.close(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a OrganizationTrustComponent
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param data The data to pass to the dialog
|
||||
*/
|
||||
static open(dialogService: DialogService, data: OrganizationTrustDialogData) {
|
||||
return dialogService.open<boolean, OrganizationTrustDialogData>(OrganizationTrustComponent, {
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormControl, FormGroup } from "@angular/forms";
|
||||
|
||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
export type UserConfirmDialogData = {
|
||||
|
||||
@@ -71,7 +71,7 @@ export abstract class BaseBulkConfirmComponent implements OnInit {
|
||||
if (publicKey == null) {
|
||||
continue;
|
||||
}
|
||||
const encryptedKey = await this.encryptService.rsaEncrypt(key.key, publicKey);
|
||||
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(key, publicKey);
|
||||
userIdsWithKeys.push({
|
||||
id: user.id,
|
||||
key: encryptedKey.encryptedString,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
@@ -21,7 +20,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@@ -9,7 +8,7 @@ import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enum
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
|
||||
import { DeleteManagedMemberWarningService } from "../../services/delete-managed-member/delete-managed-member-warning.service";
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DialogRef,
|
||||
DIALOG_DATA,
|
||||
DialogService,
|
||||
TableDataSource,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { OrganizationUserView } from "../../../core";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import {
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BaseBulkRemoveComponent } from "./base-bulk-remove.component";
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
@@ -9,7 +8,7 @@ import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enum
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DIALOG_DATA, DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
|
||||
import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common";
|
||||
@@ -13,7 +12,7 @@ import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationUserView } from "../../../core/views/organization-user.view";
|
||||
|
||||
|
||||
@@ -177,11 +177,17 @@
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
</ng-container>
|
||||
<bit-form-field>
|
||||
<bit-form-field *ngIf="isExternalIdVisible$ | async">
|
||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="externalId" />
|
||||
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field *ngIf="isSsoExternalIdVisible$ | async">
|
||||
<bit-label>{{ "ssoExternalId" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="ssoExternalId" />
|
||||
<bit-hint>{{ "ssoExternalIdDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</bit-tab>
|
||||
<bit-tab *ngIf="organization.useGroups" [label]="'groups' | i18n">
|
||||
<div class="tw-mb-6">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import {
|
||||
@@ -37,7 +36,13 @@ import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
GroupApiService,
|
||||
@@ -120,6 +125,7 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
emails: [""],
|
||||
type: OrganizationUserType.User,
|
||||
externalId: this.formBuilder.control({ value: "", disabled: true }),
|
||||
ssoExternalId: this.formBuilder.control({ value: "", disabled: true }),
|
||||
accessSecretsManager: false,
|
||||
access: [[] as AccessItemValue[]],
|
||||
groups: [[] as AccessItemValue[]],
|
||||
@@ -150,6 +156,22 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
FeatureFlag.AccountDeprovisioning,
|
||||
);
|
||||
|
||||
protected isExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(
|
||||
map((isEnabled) => {
|
||||
return !isEnabled || !!this.formGroup.get("externalId")?.value;
|
||||
}),
|
||||
);
|
||||
|
||||
protected isSsoExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(
|
||||
map((isEnabled) => {
|
||||
return isEnabled && !!this.formGroup.get("ssoExternalId")?.value;
|
||||
}),
|
||||
);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
get customUserTypeSelected(): boolean {
|
||||
@@ -397,6 +419,7 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
this.formGroup.patchValue({
|
||||
type: userDetails.type,
|
||||
externalId: userDetails.externalId,
|
||||
ssoExternalId: userDetails.ssoExternalId,
|
||||
access: accessSelections,
|
||||
accessSecretsManager: userDetails.accessSecretsManager,
|
||||
groups: groupAccessSelections,
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
@@ -81,12 +88,16 @@ export class ResetPasswordComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<ResetPasswordDialogResult>,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$()
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(
|
||||
(enforcedPasswordPolicyOptions) =>
|
||||
(this.enforcedPolicyOptions = enforcedPasswordPolicyOptions),
|
||||
|
||||
@@ -3,8 +3,10 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { FreeBitwardenFamiliesComponent } from "../../../billing/members/free-bitwarden-families.component";
|
||||
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
import { canAccessSponsoredFamilies } from "./../../../billing/guards/can-access-sponsored-families.guard";
|
||||
import { MembersComponent } from "./members.component";
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -16,6 +18,14 @@ const routes: Routes = [
|
||||
titleId: "members",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "sponsored-families",
|
||||
component: FreeBitwardenFamiliesComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessMembersTab), canAccessSponsoredFamilies],
|
||||
data: {
|
||||
titleId: "sponsoredFamilies",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -43,6 +43,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -168,15 +169,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
this.canUseSecretsManager$ = organization$.pipe(map((org) => org.useSecretsManager));
|
||||
|
||||
const policies$ = organization$.pipe(
|
||||
switchMap((organization) => {
|
||||
const policies$ = combineLatest([
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
organization$,
|
||||
]).pipe(
|
||||
switchMap(([userId, organization]) => {
|
||||
if (organization.isProviderUser) {
|
||||
return from(this.policyApiService.getPolicies(organization.id)).pipe(
|
||||
map((response) => Policy.fromListResponse(response)),
|
||||
);
|
||||
}
|
||||
|
||||
return this.policyService.policies$;
|
||||
return this.policyService.policies$(userId);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -318,7 +322,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
|
||||
const orgKey = await this.keyService.getOrgKey(this.organization.id);
|
||||
const key = await this.encryptService.rsaEncrypt(orgKey.key, publicKey);
|
||||
const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey);
|
||||
const request = new OrganizationUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
await this.organizationUserApiService.postOrganizationUserConfirm(
|
||||
|
||||
@@ -50,7 +50,7 @@ export class DeleteManagedMemberWarningService {
|
||||
key: "deleteManagedUserWarningDesc",
|
||||
},
|
||||
type: "danger",
|
||||
icon: "bwi-exclamation-circle",
|
||||
icon: "bwi-exclamation-triangle",
|
||||
acceptButtonText: { key: "continue" },
|
||||
cancelButtonText: { key: "cancel" },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export class OrganizationUserResetPasswordEntry {
|
||||
orgId: string;
|
||||
publicKey: Uint8Array;
|
||||
orgName: string;
|
||||
|
||||
constructor(orgId: string, publicKey: Uint8Array, orgName: string) {
|
||||
this.orgId = orgId;
|
||||
this.publicKey = publicKey;
|
||||
this.orgName = orgName;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
@@ -14,6 +14,7 @@ import { OrganizationApiService } from "@bitwarden/common/admin-console/services
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
@@ -23,6 +24,9 @@ import { KdfType, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "./organization-user-reset-password.service";
|
||||
|
||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as UserKey;
|
||||
const mockPublicKeys = [Utils.fromUtf8ToArray("test-public-key")];
|
||||
|
||||
describe("OrganizationUserResetPasswordService", () => {
|
||||
let sut: OrganizationUserResetPasswordService;
|
||||
|
||||
@@ -51,6 +55,21 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
organizationService.organizations$.mockReturnValue(
|
||||
new BehaviorSubject([
|
||||
createOrganization("1", "org1", true),
|
||||
createOrganization("2", "org2", false),
|
||||
]),
|
||||
);
|
||||
organizationApiService.getKeys.mockResolvedValue(
|
||||
new OrganizationKeysResponse({
|
||||
privateKey: "privateKey",
|
||||
publicKey: "publicKey",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
@@ -59,54 +78,49 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
expect(sut).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("getRecoveryKey", () => {
|
||||
describe("buildRecoveryKey", () => {
|
||||
const mockOrgId = "test-org-id";
|
||||
|
||||
beforeEach(() => {
|
||||
organizationApiService.getKeys.mockResolvedValue(
|
||||
new OrganizationKeysResponse({
|
||||
privateKey: "test-private-key",
|
||||
publicKey: "test-public-key",
|
||||
publicKey: Utils.fromUtf8ToArray("test-public-key"),
|
||||
}),
|
||||
);
|
||||
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||
keyService.getUserKey.mockResolvedValue(mockUserKey);
|
||||
|
||||
encryptService.rsaEncrypt.mockResolvedValue(
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(
|
||||
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return an encrypted user key", async () => {
|
||||
const encryptedString = await sut.buildRecoveryKey(mockOrgId);
|
||||
const encryptedString = await sut.buildRecoveryKey(mockOrgId, mockUserKey, mockPublicKeys);
|
||||
expect(encryptedString).toBeDefined();
|
||||
});
|
||||
|
||||
it("should only use the user key from memory if one is not provided", async () => {
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||
|
||||
await sut.buildRecoveryKey(mockOrgId, mockUserKey);
|
||||
|
||||
expect(keyService.getUserKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw an error if the organization keys are null", async () => {
|
||||
organizationApiService.getKeys.mockResolvedValue(null);
|
||||
await expect(sut.buildRecoveryKey(mockOrgId)).rejects.toThrow();
|
||||
await expect(sut.buildRecoveryKey(mockOrgId, mockUserKey, mockPublicKeys)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should throw an error if the user key can't be found", async () => {
|
||||
keyService.getUserKey.mockResolvedValue(null);
|
||||
await expect(sut.buildRecoveryKey(mockOrgId)).rejects.toThrow();
|
||||
await expect(sut.buildRecoveryKey(mockOrgId, null, mockPublicKeys)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should rsa encrypt the user key", async () => {
|
||||
await sut.buildRecoveryKey(mockOrgId);
|
||||
await sut.buildRecoveryKey(mockOrgId, mockUserKey, mockPublicKeys);
|
||||
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(expect.anything(), expect.anything());
|
||||
it("should throw an error if the public key is not trusted", async () => {
|
||||
await expect(
|
||||
sut.buildRecoveryKey(mockOrgId, mockUserKey, [new Uint8Array(64)]),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,6 +177,20 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPublicKeys", () => {
|
||||
it("should return public keys for organizations that have reset password enrolled", async () => {
|
||||
const result = await sut.getPublicKeys("userId" as UserId);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should result should contain the correct data for the org", async () => {
|
||||
const result = await sut.getPublicKeys("userId" as UserId);
|
||||
expect(result[0].orgId).toBe("1");
|
||||
expect(result[0].orgName).toBe("org1");
|
||||
expect(result[0].publicKey).toEqual(Utils.fromB64ToArray("publicKey"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRotatedData", () => {
|
||||
beforeEach(() => {
|
||||
organizationService.organizations$.mockReturnValue(
|
||||
@@ -171,10 +199,10 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
organizationApiService.getKeys.mockResolvedValue(
|
||||
new OrganizationKeysResponse({
|
||||
privateKey: "test-private-key",
|
||||
publicKey: "test-public-key",
|
||||
publicKey: Utils.fromUtf8ToArray("test-public-key"),
|
||||
}),
|
||||
);
|
||||
encryptService.rsaEncrypt.mockResolvedValue(
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(
|
||||
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "mockEncryptedUserKey"),
|
||||
);
|
||||
});
|
||||
@@ -182,7 +210,7 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
it("should return all re-encrypted account recovery keys", async () => {
|
||||
const result = await sut.getRotatedData(
|
||||
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
|
||||
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
|
||||
mockPublicKeys,
|
||||
"mockUserId" as UserId,
|
||||
);
|
||||
|
||||
@@ -191,22 +219,18 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
|
||||
it("throws if the new user key is null", async () => {
|
||||
await expect(
|
||||
sut.getRotatedData(
|
||||
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
|
||||
null,
|
||||
"mockUserId" as UserId,
|
||||
),
|
||||
sut.getRotatedData(null, mockPublicKeys, "mockUserId" as UserId),
|
||||
).rejects.toThrow("New user key is required for rotation.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createOrganization(id: string, name: string) {
|
||||
function createOrganization(id: string, name: string, resetPasswordEnrolled = true): Organization {
|
||||
const org = new Organization();
|
||||
org.id = id;
|
||||
org.name = name;
|
||||
org.identifier = name;
|
||||
org.isMember = true;
|
||||
org.resetPasswordEnrolled = true;
|
||||
org.resetPasswordEnrolled = resetPasswordEnrolled;
|
||||
return org;
|
||||
}
|
||||
|
||||
@@ -14,23 +14,28 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString, 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 { UserKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
PBKDF2KdfConfig,
|
||||
UserKeyRotationDataProvider,
|
||||
UserKeyRotationKeyRecoveryProvider,
|
||||
KeyService,
|
||||
KdfType,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserResetPasswordEntry } from "./organization-user-reset-password-entry";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class OrganizationUserResetPasswordService
|
||||
implements UserKeyRotationDataProvider<OrganizationUserResetPasswordWithIdRequest>
|
||||
implements
|
||||
UserKeyRotationKeyRecoveryProvider<
|
||||
OrganizationUserResetPasswordWithIdRequest,
|
||||
OrganizationUserResetPasswordEntry
|
||||
>
|
||||
{
|
||||
constructor(
|
||||
private keyService: KeyService,
|
||||
@@ -42,11 +47,21 @@ export class OrganizationUserResetPasswordService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the user key encrypted by the organization's public key.
|
||||
* Intended for use in enrollment
|
||||
* Builds a recovery key for a user to recover their account.
|
||||
*
|
||||
* @param orgId desired organization
|
||||
* @param userKey user key
|
||||
* @param trustedPublicKeys public keys of organizations that the user trusts
|
||||
*/
|
||||
async buildRecoveryKey(orgId: string, userKey?: UserKey): Promise<EncryptedString> {
|
||||
async buildRecoveryKey(
|
||||
orgId: string,
|
||||
userKey: UserKey,
|
||||
trustedPublicKeys: Uint8Array[],
|
||||
): Promise<EncryptedString> {
|
||||
if (userKey == null) {
|
||||
throw new Error("User key is required for recovery.");
|
||||
}
|
||||
|
||||
// Retrieve Public Key
|
||||
const orgKeys = await this.organizationApiService.getKeys(orgId);
|
||||
if (orgKeys == null) {
|
||||
@@ -55,12 +70,16 @@ export class OrganizationUserResetPasswordService
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(orgKeys.publicKey);
|
||||
|
||||
// RSA Encrypt user key with organization's public key
|
||||
userKey ??= await this.keyService.getUserKey();
|
||||
if (userKey == null) {
|
||||
throw new Error("No user key found");
|
||||
if (
|
||||
!trustedPublicKeys.some(
|
||||
(key) => Utils.fromBufferToHex(key) === Utils.fromBufferToHex(publicKey),
|
||||
)
|
||||
) {
|
||||
throw new Error("Untrusted public key");
|
||||
}
|
||||
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
|
||||
|
||||
// RSA Encrypt user key with organization's public key
|
||||
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey);
|
||||
|
||||
return encryptedKey.encryptedString;
|
||||
}
|
||||
@@ -99,11 +118,11 @@ export class OrganizationUserResetPasswordService
|
||||
);
|
||||
|
||||
// Decrypt User's Reset Password Key to get UserKey
|
||||
const decValue = await this.encryptService.rsaDecrypt(
|
||||
const userKey = await this.encryptService.decapsulateKeyUnsigned(
|
||||
new EncString(response.resetPasswordKey),
|
||||
decPrivateKey,
|
||||
);
|
||||
const existingUserKey = new SymmetricCryptoKey(decValue) as UserKey;
|
||||
const existingUserKey = userKey as UserKey;
|
||||
|
||||
// determine Kdf Algorithm
|
||||
const kdfConfig: KdfConfig =
|
||||
@@ -138,6 +157,21 @@ export class OrganizationUserResetPasswordService
|
||||
);
|
||||
}
|
||||
|
||||
async getPublicKeys(userId: UserId): Promise<OrganizationUserResetPasswordEntry[]> {
|
||||
const allOrgs = (await firstValueFrom(this.organizationService.organizations$(userId))).filter(
|
||||
(org) => org.resetPasswordEnrolled,
|
||||
);
|
||||
|
||||
const entries: OrganizationUserResetPasswordEntry[] = [];
|
||||
for (const org of allOrgs) {
|
||||
const publicKey = await this.organizationApiService.getKeys(org.id);
|
||||
const encodedPublicKey = Utils.fromB64ToArray(publicKey.publicKey);
|
||||
const entry = new OrganizationUserResetPasswordEntry(org.id, encodedPublicKey, org.name);
|
||||
entries.push(entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns existing account recovery keys re-encrypted with the new user key.
|
||||
* @param originalUserKey the original user key
|
||||
@@ -147,8 +181,8 @@ export class OrganizationUserResetPasswordService
|
||||
* @returns a list of account recovery keys that have been re-encrypted with the new user key
|
||||
*/
|
||||
async getRotatedData(
|
||||
originalUserKey: UserKey,
|
||||
newUserKey: UserKey,
|
||||
trustedPublicKeys: Uint8Array[],
|
||||
userId: UserId,
|
||||
): Promise<OrganizationUserResetPasswordWithIdRequest[] | null> {
|
||||
if (newUserKey == null) {
|
||||
@@ -156,9 +190,8 @@ export class OrganizationUserResetPasswordService
|
||||
}
|
||||
|
||||
const allOrgs = await firstValueFrom(this.organizationService.organizations$(userId));
|
||||
|
||||
if (!allOrgs) {
|
||||
return;
|
||||
throw new Error("Could not get organizations");
|
||||
}
|
||||
|
||||
const requests: OrganizationUserResetPasswordWithIdRequest[] = [];
|
||||
@@ -169,7 +202,7 @@ export class OrganizationUserResetPasswordService
|
||||
}
|
||||
|
||||
// Re-enroll - encrypt user key with organization public key
|
||||
const encryptedKey = await this.buildRecoveryKey(org.id, newUserKey);
|
||||
const encryptedKey = await this.buildRecoveryKey(org.id, newUserKey, trustedPublicKeys);
|
||||
|
||||
// Create/Execute request
|
||||
const request = new OrganizationUserResetPasswordWithIdRequest();
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
<app-header></app-header>
|
||||
<app-header>
|
||||
@let organization = organization$ | async;
|
||||
<button
|
||||
bitBadge
|
||||
class="!tw-align-middle"
|
||||
(click)="changePlan(organization)"
|
||||
*ngIf="isBreadcrumbingEnabled$ | async"
|
||||
slot="title-suffix"
|
||||
type="button"
|
||||
variant="primary"
|
||||
>
|
||||
{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
</app-header>
|
||||
|
||||
<bit-container>
|
||||
<ng-container *ngIf="loading">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { firstValueFrom, lastValueFrom } from "rxjs";
|
||||
import { first, map } from "rxjs/operators";
|
||||
import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -14,10 +14,17 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
ChangePlanDialogResultType,
|
||||
openChangePlanDialog,
|
||||
} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component";
|
||||
import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
import { PolicyListService } from "../../core/policy-list.service";
|
||||
import { BasePolicy } from "../policies";
|
||||
import { CollectionDialogTabType } from "../shared/components/collection-dialog";
|
||||
|
||||
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
|
||||
|
||||
@@ -32,17 +39,19 @@ export class PoliciesComponent implements OnInit {
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
policies: BasePolicy[];
|
||||
organization: Organization;
|
||||
protected organization$: Observable<Organization>;
|
||||
|
||||
private orgPolicies: PolicyResponse[];
|
||||
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
protected isBreadcrumbingEnabled$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private organizationService: OrganizationService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private policyListService: PolicyListService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
@@ -53,11 +62,9 @@ export class PoliciesComponent implements OnInit {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
this.organization$ = this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId));
|
||||
this.policies = this.policyListService.getPolicies();
|
||||
|
||||
await this.load();
|
||||
@@ -91,7 +98,11 @@ export class PoliciesComponent implements OnInit {
|
||||
this.orgPolicies.forEach((op) => {
|
||||
this.policiesEnabledMap.set(op.type, op.enabled);
|
||||
});
|
||||
|
||||
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
|
||||
),
|
||||
);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@@ -104,8 +115,34 @@ export class PoliciesComponent implements OnInit {
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === PolicyEditDialogResult.Saved) {
|
||||
await this.load();
|
||||
switch (result) {
|
||||
case PolicyEditDialogResult.Saved:
|
||||
await this.load();
|
||||
break;
|
||||
case PolicyEditDialogResult.UpgradePlan:
|
||||
await this.changePlan(await firstValueFrom(this.organization$));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly CollectionDialogTabType = CollectionDialogTabType;
|
||||
protected readonly All = All;
|
||||
|
||||
protected async changePlan(organization: Organization) {
|
||||
const reference = openChangePlanDialog(this.dialogService, {
|
||||
data: {
|
||||
organizationId: organization.id,
|
||||
subscription: null,
|
||||
productTierType: organization.productTierType,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(reference.closed);
|
||||
|
||||
if (result === ChangePlanDialogResultType.Closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
|
||||
<ng-container bitDialogTitle>
|
||||
<button
|
||||
bitBadge
|
||||
class="!tw-align-middle"
|
||||
(click)="upgradePlan()"
|
||||
*ngIf="isBreadcrumbingEnabled$ | async"
|
||||
type="button"
|
||||
variant="primary"
|
||||
>
|
||||
{{ "planNameEnterprise" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container bitDialogContent>
|
||||
<div *ngIf="loading">
|
||||
<i
|
||||
@@ -16,6 +28,7 @@
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
*ngIf="!(isBreadcrumbingEnabled$ | async); else breadcrumbing"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="saveDisabled$ | async"
|
||||
@@ -24,6 +37,11 @@
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<ng-template #breadcrumbing>
|
||||
<button bitButton buttonType="primary" bitFormButton type="button" (click)="upgradePlan()">
|
||||
{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
</ng-template>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectorRef,
|
||||
@@ -10,14 +9,28 @@ import {
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { Observable, map } from "rxjs";
|
||||
import { map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "../policies";
|
||||
|
||||
@@ -30,6 +43,7 @@ export type PolicyEditDialogData = {
|
||||
|
||||
export enum PolicyEditDialogResult {
|
||||
Saved = "saved",
|
||||
UpgradePlan = "upgrade-plan",
|
||||
}
|
||||
@Component({
|
||||
selector: "app-policy-edit",
|
||||
@@ -43,22 +57,28 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
loading = true;
|
||||
enabled = false;
|
||||
saveDisabled$: Observable<boolean>;
|
||||
defaultTypes: any[];
|
||||
policyComponent: BasePolicyComponent;
|
||||
|
||||
private policyResponse: PolicyResponse;
|
||||
formGroup = this.formBuilder.group({
|
||||
enabled: [this.enabled],
|
||||
});
|
||||
protected organization$: Observable<Organization>;
|
||||
protected isBreadcrumbingEnabled$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
|
||||
private accountService: AccountService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private organizationService: OrganizationService,
|
||||
private i18nService: I18nService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<PolicyEditDialogResult>,
|
||||
private toastService: ToastService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
) {}
|
||||
|
||||
get policy(): BasePolicy {
|
||||
return this.data.policy;
|
||||
}
|
||||
@@ -92,6 +112,16 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
this.organization$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.organizationService.organizations$(userId)),
|
||||
getOrganizationById(this.data.organizationId),
|
||||
);
|
||||
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
|
||||
switchMap((organization) =>
|
||||
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
@@ -114,4 +144,8 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
|
||||
return dialogService.open<PolicyEditDialogResult>(PolicyEditComponent, config);
|
||||
};
|
||||
|
||||
protected upgradePlan(): void {
|
||||
this.dialogRef.close(PolicyEditDialogResult.UpgradePlan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{{ "keyConnectorPolicyRestriction" | i18n }}
|
||||
</bit-callout>
|
||||
|
||||
<bit-callout type="success" [title]="'prerequisite' | i18n" icon="bwi-lightbulb">
|
||||
<bit-callout type="info" [title]="'prerequisite' | i18n">
|
||||
{{ "accountRecoverySingleOrgRequirementDesc" | i18n }}
|
||||
</bit-callout>
|
||||
|
||||
|
||||
@@ -42,12 +42,14 @@
|
||||
{{ "learnMoreAboutApi" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<form
|
||||
*ngIf="org && !loading"
|
||||
@@ -69,7 +71,7 @@
|
||||
<input type="checkbox" bitCheckbox formControlName="limitCollectionDeletion" />
|
||||
</bit-form-control>
|
||||
<bit-form-control *ngIf="limitItemDeletionFeatureFlagIsEnabled">
|
||||
<bit-label>{{ "limitItemDeletionDesc" | i18n }}</bit-label>
|
||||
<bit-label>{{ "limitItemDeletionDescription" | i18n }}</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="limitItemDeletion" />
|
||||
</bit-form-control>
|
||||
<button
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
</p>
|
||||
<app-user-verification formControlName="secret"> </app-user-verification>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="danger" [disabled]="!loaded">
|
||||
{{ "deleteOrganization" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||
import { combineLatest, firstValueFrom, Subject, takeUntil } from "rxjs";
|
||||
@@ -21,7 +20,13 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { UserVerificationModule } from "../../../../auth/shared/components/user-verification";
|
||||
import { SharedModule } from "../../../../shared/shared.module";
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { NgModule, inject } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
|
||||
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
|
||||
@@ -41,7 +43,14 @@ const routes: Routes = [
|
||||
{
|
||||
path: "policies",
|
||||
component: PoliciesComponent,
|
||||
canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)],
|
||||
canActivate: [
|
||||
organizationPermissionsGuard((o: Organization) => {
|
||||
const organizationBillingService = inject(OrganizationBillingServiceAbstraction);
|
||||
return organizationBillingService
|
||||
.isBreadcrumbingPoliciesEnabled$(o)
|
||||
.pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled));
|
||||
}),
|
||||
],
|
||||
data: {
|
||||
titleId: "policies",
|
||||
},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { ItemModule } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule, SharedModule } from "../../../shared";
|
||||
import { AccountFingerprintComponent } from "../../../shared/components/account-fingerprint/account-fingerprint.component";
|
||||
import { PoliciesModule } from "../../organizations/policies";
|
||||
@@ -15,6 +17,7 @@ import { TwoFactorSetupComponent } from "./two-factor-setup.component";
|
||||
PoliciesModule,
|
||||
OrganizationSettingsRoutingModule,
|
||||
AccountFingerprintComponent,
|
||||
ItemModule,
|
||||
],
|
||||
declarations: [AccountComponent, TwoFactorSetupComponent],
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, takeUntil, map, lastValueFrom, firstValueFrom } from "rxjs";
|
||||
@@ -21,7 +20,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DialogRef, DialogService } from "@bitwarden/components";
|
||||
|
||||
import { TwoFactorSetupDuoComponent } from "../../../auth/settings/two-factor/two-factor-setup-duo.component";
|
||||
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor/two-factor-setup.component";
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-form-field *ngIf="isExternalIdVisible$ | async">
|
||||
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="externalId" />
|
||||
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||
import { AbstractControl, FormBuilder, Validators } from "@angular/forms";
|
||||
import {
|
||||
@@ -13,6 +12,8 @@ import {
|
||||
Subject,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
filter,
|
||||
} from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
@@ -39,7 +40,15 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SelectModule, BitValidators, DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
SelectModule,
|
||||
BitValidators,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { openChangePlanDialog } from "../../../../../billing/organizations/change-plan-dialog.component";
|
||||
import { SharedModule } from "../../../../../shared";
|
||||
@@ -86,6 +95,7 @@ export interface CollectionDialogParams {
|
||||
limitNestedCollections?: boolean;
|
||||
readonly?: boolean;
|
||||
isAddAccessCollection?: boolean;
|
||||
isAdminConsoleActive?: boolean;
|
||||
}
|
||||
|
||||
export interface CollectionDialogResult {
|
||||
@@ -129,6 +139,16 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
protected showAddAccessWarning = false;
|
||||
protected collections: Collection[];
|
||||
protected buttonDisplayName: ButtonType = ButtonType.Save;
|
||||
protected isExternalIdVisible$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(
|
||||
map((isEnabled) => {
|
||||
return (
|
||||
!isEnabled ||
|
||||
(!!this.params.isAdminConsoleActive && !!this.formGroup.get("externalId")?.value)
|
||||
);
|
||||
}),
|
||||
);
|
||||
private orgExceedingCollectionLimit!: Organization;
|
||||
|
||||
constructor(
|
||||
@@ -189,10 +209,29 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
this.formGroup.updateValueAndValidity();
|
||||
}
|
||||
|
||||
this.organizationSelected.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((_) => {
|
||||
this.organizationSelected.markAsTouched();
|
||||
this.formGroup.updateValueAndValidity();
|
||||
});
|
||||
this.organizationSelected.valueChanges
|
||||
.pipe(
|
||||
tap((_) => {
|
||||
if (this.organizationSelected.errors?.cannotCreateCollections) {
|
||||
this.buttonDisplayName = ButtonType.Upgrade;
|
||||
} else {
|
||||
this.buttonDisplayName = ButtonType.Save;
|
||||
}
|
||||
}),
|
||||
filter(() => this.organizationSelected.errors?.cannotCreateCollections),
|
||||
switchMap((value) => this.findOrganizationById(value)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((org) => {
|
||||
this.orgExceedingCollectionLimit = org;
|
||||
this.organizationSelected.markAsTouched();
|
||||
this.formGroup.updateValueAndValidity();
|
||||
});
|
||||
}
|
||||
|
||||
async findOrganizationById(orgId: string): Promise<Organization | undefined> {
|
||||
const organizations = await firstValueFrom(this.organizations$);
|
||||
return organizations.find((org) => org.id === orgId);
|
||||
}
|
||||
|
||||
async loadOrg(orgId: string) {
|
||||
@@ -450,7 +489,18 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
this.formGroup.controls.access.disable();
|
||||
} else {
|
||||
this.formGroup.controls.name.enable();
|
||||
this.formGroup.controls.externalId.enable();
|
||||
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((isEnabled) => {
|
||||
if (isEnabled) {
|
||||
this.formGroup.controls.externalId.disable();
|
||||
} else {
|
||||
this.formGroup.controls.externalId.enable();
|
||||
}
|
||||
});
|
||||
|
||||
this.formGroup.controls.parent.enable();
|
||||
this.formGroup.controls.access.enable();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<div class="tw-mt-10 tw-flex tw-justify-center" *ngIf="loading">
|
||||
<div>
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo"></bit-icon>
|
||||
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n">
|
||||
</bit-icon>
|
||||
<div class="tw-flex tw-justify-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
|
||||
@@ -43,6 +43,8 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
|
||||
value.plan = PlanType.FamiliesAnnually;
|
||||
value.productTier = ProductTierType.Families;
|
||||
value.acceptingSponsorship = true;
|
||||
value.planSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise;
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
value.onSuccess.subscribe(this.onOrganizationCreateSuccess.bind(this));
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, lastValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification";
|
||||
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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationTrustComponent } from "../manage/organization-trust.component";
|
||||
import { OrganizationUserResetPasswordService } from "../members/services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
|
||||
interface EnrollMasterPasswordResetData {
|
||||
@@ -28,12 +34,14 @@ export class EnrollMasterPasswordReset {
|
||||
data: EnrollMasterPasswordResetData,
|
||||
resetPasswordService: OrganizationUserResetPasswordService,
|
||||
organizationUserApiService: OrganizationUserApiService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
syncService: SyncService,
|
||||
logService: LogService,
|
||||
userVerificationService: UserVerificationService,
|
||||
toastService: ToastService,
|
||||
keyService: KeyService,
|
||||
accountService: AccountService,
|
||||
organizationApiService: OrganizationApiServiceAbstraction,
|
||||
) {
|
||||
const result = await UserVerificationDialogComponent.open(dialogService, {
|
||||
title: "enrollAccountRecovery",
|
||||
@@ -44,12 +52,33 @@ export class EnrollMasterPasswordReset {
|
||||
verificationType: {
|
||||
type: "custom",
|
||||
verificationFn: async (secret: VerificationWithSecret) => {
|
||||
const activeUserId = (await firstValueFrom(accountService.activeAccount$)).id;
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(
|
||||
(await organizationApiService.getKeys(data.organization.id)).publicKey,
|
||||
);
|
||||
|
||||
const request =
|
||||
await userVerificationService.buildRequest<OrganizationUserResetPasswordEnrollmentRequest>(
|
||||
secret,
|
||||
);
|
||||
const dialogRef = OrganizationTrustComponent.open(dialogService, {
|
||||
name: data.organization.name,
|
||||
orgId: data.organization.id,
|
||||
publicKey,
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result !== true) {
|
||||
throw new Error("Organization not trusted, aborting user key rotation");
|
||||
}
|
||||
|
||||
const trustedOrgPublicKeys = [publicKey];
|
||||
const userKey = await firstValueFrom(keyService.userKey$(activeUserId));
|
||||
|
||||
request.resetPasswordKey = await resetPasswordService.buildRecoveryKey(
|
||||
data.organization.id,
|
||||
userKey,
|
||||
trustedOrgPublicKeys,
|
||||
);
|
||||
|
||||
// Process the enrollment request, which is an endpoint that is
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./webauthn-login";
|
||||
export * from "./set-password-jit";
|
||||
export * from "./registration";
|
||||
export * from "./two-factor-auth";
|
||||
export * from "./link-sso.service";
|
||||
|
||||
154
apps/web/src/app/auth/core/services/link-sso.service.spec.ts
Normal file
154
apps/web/src/app/auth/core/services/link-sso.service.spec.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordGeneratorOptions,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
|
||||
import { LinkSsoService } from "./link-sso.service";
|
||||
|
||||
describe("LinkSsoService", () => {
|
||||
let sut: LinkSsoService;
|
||||
|
||||
let mockSsoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let mockApiService: MockProxy<ApiService>;
|
||||
let mockCryptoFunctionService: MockProxy<CryptoFunctionService>;
|
||||
let mockEnvironmentService: MockProxy<EnvironmentService>;
|
||||
let mockPasswordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let mockPlatformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
|
||||
const mockEnvironment$ = new BehaviorSubject<any>({
|
||||
getIdentityUrl: jest.fn().mockReturnValue("https://identity.bitwarden.com"),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock implementations
|
||||
mockSsoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
mockApiService = mock<ApiService>();
|
||||
mockCryptoFunctionService = mock<CryptoFunctionService>();
|
||||
mockEnvironmentService = mock<EnvironmentService>();
|
||||
mockPasswordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||
|
||||
// Set up environment service to return our mock environment
|
||||
mockEnvironmentService.environment$ = mockEnvironment$;
|
||||
|
||||
// Set up API service mocks
|
||||
const mockResponse = { Token: "mockSsoToken" };
|
||||
mockApiService.preValidateSso.mockResolvedValue(new SsoPreValidateResponse(mockResponse));
|
||||
mockApiService.getSsoUserIdentifier.mockResolvedValue("mockUserIdentifier");
|
||||
|
||||
// Set up password generation service mock
|
||||
mockPasswordGenerationService.generatePassword.mockImplementation(
|
||||
async (options: PasswordGeneratorOptions) => {
|
||||
return "mockGeneratedPassword";
|
||||
},
|
||||
);
|
||||
|
||||
// Set up crypto function service mock
|
||||
mockCryptoFunctionService.hash.mockResolvedValue(new Uint8Array([1, 2, 3, 4]));
|
||||
|
||||
// Create the service under test with mock dependencies
|
||||
sut = new LinkSsoService(
|
||||
mockSsoLoginService,
|
||||
mockApiService,
|
||||
mockCryptoFunctionService,
|
||||
mockEnvironmentService,
|
||||
mockPasswordGenerationService,
|
||||
mockPlatformUtilsService,
|
||||
);
|
||||
|
||||
// Mock Utils.fromBufferToUrlB64
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue("mockCodeChallenge");
|
||||
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
origin: "https://bitwarden.com",
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("linkSso", () => {
|
||||
it("throws an error when identifier is null", async () => {
|
||||
await expect(sut.linkSso(null as unknown as string)).rejects.toThrow(
|
||||
"SSO identifier is required",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws an error when identifier is empty", async () => {
|
||||
await expect(sut.linkSso("")).rejects.toThrow("SSO identifier is required");
|
||||
});
|
||||
|
||||
it("calls preValidateSso with the provided identifier", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockApiService.preValidateSso).toHaveBeenCalledWith("org123");
|
||||
});
|
||||
|
||||
it("generates a password for code verifier", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockPasswordGenerationService.generatePassword).toHaveBeenCalledWith({
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
number: true,
|
||||
special: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("sets the code verifier in the ssoLoginService", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockSsoLoginService.setCodeVerifier).toHaveBeenCalledWith("mockGeneratedPassword");
|
||||
});
|
||||
|
||||
it("generates a state and sets it in the ssoLoginService", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
const expectedState =
|
||||
"mockGeneratedPassword_returnUri='/settings/organizations'_identifier=org123";
|
||||
expect(mockSsoLoginService.setSsoState).toHaveBeenCalledWith(expectedState);
|
||||
});
|
||||
|
||||
it("gets the SSO user identifier from the API", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockApiService.getSsoUserIdentifier).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("launches the authorize URL with the correct parameters", async () => {
|
||||
await sut.linkSso("org123");
|
||||
|
||||
expect(mockPlatformUtilsService.launchUri).toHaveBeenCalledWith(
|
||||
expect.stringContaining("https://identity.bitwarden.com/connect/authorize"),
|
||||
{ sameWindow: true },
|
||||
);
|
||||
|
||||
const launchUriArg = mockPlatformUtilsService.launchUri.mock.calls[0][0];
|
||||
expect(launchUriArg).toContain("client_id=web");
|
||||
expect(launchUriArg).toContain(
|
||||
"redirect_uri=https%3A%2F%2Fbitwarden.com%2Fsso-connector.html",
|
||||
);
|
||||
expect(launchUriArg).toContain("response_type=code");
|
||||
expect(launchUriArg).toContain("code_challenge=mockCodeChallenge");
|
||||
expect(launchUriArg).toContain("ssoToken=mockSsoToken");
|
||||
expect(launchUriArg).toContain("user_identifier=mockUserIdentifier");
|
||||
});
|
||||
});
|
||||
});
|
||||
91
apps/web/src/app/auth/core/services/link-sso.service.ts
Normal file
91
apps/web/src/app/auth/core/services/link-sso.service.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
PasswordGeneratorOptions,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
|
||||
/**
|
||||
* Provides a service for linking SSO.
|
||||
*/
|
||||
export class LinkSsoService {
|
||||
constructor(
|
||||
private ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private apiService: ApiService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private environmentService: EnvironmentService,
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Links SSO to an organization.
|
||||
* Ported from the SsoComponent
|
||||
* @param identifier The identifier of the organization to link to.
|
||||
*/
|
||||
async linkSso(identifier: string) {
|
||||
if (identifier == null || identifier === "") {
|
||||
throw new Error("SSO identifier is required");
|
||||
}
|
||||
|
||||
const redirectUri = window.location.origin + "/sso-connector.html";
|
||||
const clientId = "web";
|
||||
const returnUri = "/settings/organizations";
|
||||
|
||||
const response = await this.apiService.preValidateSso(identifier);
|
||||
|
||||
const passwordOptions: PasswordGeneratorOptions = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
number: true,
|
||||
special: false,
|
||||
};
|
||||
|
||||
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
await this.ssoLoginService.setCodeVerifier(codeVerifier);
|
||||
|
||||
let state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
state += `_returnUri='${returnUri}'`;
|
||||
state += `_identifier=${identifier}`;
|
||||
|
||||
// Save state
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
|
||||
let authorizeUrl =
|
||||
env.getIdentityUrl() +
|
||||
"/connect/authorize?" +
|
||||
"client_id=" +
|
||||
clientId +
|
||||
"&redirect_uri=" +
|
||||
encodeURIComponent(redirectUri) +
|
||||
"&" +
|
||||
"response_type=code&scope=api offline_access&" +
|
||||
"state=" +
|
||||
state +
|
||||
"&code_challenge=" +
|
||||
codeChallenge +
|
||||
"&" +
|
||||
"code_challenge_method=S256&response_mode=query&" +
|
||||
"domain_hint=" +
|
||||
encodeURIComponent(identifier) +
|
||||
"&ssoToken=" +
|
||||
encodeURIComponent(response.token);
|
||||
|
||||
const userIdentifier = await this.apiService.getSsoUserIdentifier();
|
||||
authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`;
|
||||
|
||||
this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true });
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,15 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
@@ -38,6 +42,8 @@ describe("WebLoginComponentService", () => {
|
||||
let passwordGenerationService: MockProxy<PasswordGenerationServiceAbstraction>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
beforeEach(() => {
|
||||
acceptOrganizationInviteService = mock<AcceptOrganizationInviteService>();
|
||||
@@ -50,6 +56,7 @@ describe("WebLoginComponentService", () => {
|
||||
passwordGenerationService = mock<PasswordGenerationServiceAbstraction>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -65,6 +72,7 @@ describe("WebLoginComponentService", () => {
|
||||
{ provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService },
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: SsoLoginServiceAbstraction, useValue: ssoLoginService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(WebLoginComponentService);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
DefaultLoginComponentService,
|
||||
@@ -12,8 +12,10 @@ import {
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -39,6 +41,7 @@ export class WebLoginComponentService
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
cryptoFunctionService,
|
||||
@@ -93,7 +96,10 @@ export class WebLoginComponentService
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||
|
||||
const enforcedPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(policies),
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -10,9 +10,12 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -30,6 +33,8 @@ describe("WebRegistrationFinishService", () => {
|
||||
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = mock<KeyService>();
|
||||
@@ -38,6 +43,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
policyApiService = mock<PolicyApiServiceAbstraction>();
|
||||
logService = mock<LogService>();
|
||||
policyService = mock<PolicyService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
service = new WebRegistrationFinishService(
|
||||
keyService,
|
||||
@@ -46,6 +52,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
policyApiService,
|
||||
logService,
|
||||
policyService,
|
||||
accountService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -180,11 +187,11 @@ describe("WebRegistrationFinishService", () => {
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
password: "password",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
@@ -232,7 +239,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: emailVerificationToken,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
@@ -270,7 +277,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
@@ -313,7 +320,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
@@ -358,7 +365,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
@@ -405,7 +412,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
@@ -30,6 +31,7 @@ export class WebRegistrationFinishService
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private policyService: PolicyService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(keyService, accountApiService);
|
||||
}
|
||||
@@ -68,7 +70,7 @@ export class WebRegistrationFinishService
|
||||
}
|
||||
|
||||
const masterPasswordPolicyOpts: MasterPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(policies),
|
||||
this.policyService.masterPasswordPolicyOptions$(null, policies),
|
||||
);
|
||||
|
||||
return masterPasswordPolicyOpts;
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("RotateableKeySetService", () => {
|
||||
const encryptedPrivateKey = Symbol();
|
||||
keyService.makeKeyPair.mockResolvedValue(["publicKey", encryptedPrivateKey as any]);
|
||||
keyService.getUserKey.mockResolvedValue({ key: userKey.key } as any);
|
||||
encryptService.rsaEncrypt.mockResolvedValue(encryptedUserKey as any);
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedUserKey as any);
|
||||
encryptService.encrypt.mockResolvedValue(encryptedPublicKey as any);
|
||||
|
||||
const result = await service.createKeySet(externalKey as any);
|
||||
|
||||
@@ -25,7 +25,10 @@ export class RotateableKeySetService {
|
||||
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
const rawPublicKey = Utils.fromB64ToArray(publicKey);
|
||||
const encryptedUserKey = await this.encryptService.rsaEncrypt(userKey.key, rawPublicKey);
|
||||
const encryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||
userKey,
|
||||
rawPublicKey,
|
||||
);
|
||||
const encryptedPublicKey = await this.encryptService.encrypt(rawPublicKey, userKey);
|
||||
return new RotateableKeySet(encryptedUserKey, encryptedPublicKey, encryptedPrivateKey);
|
||||
}
|
||||
@@ -60,7 +63,10 @@ export class RotateableKeySetService {
|
||||
throw new Error("failed to rotate key set: could not decrypt public key");
|
||||
}
|
||||
const newEncryptedPublicKey = await this.encryptService.encrypt(publicKey, newUserKey);
|
||||
const newEncryptedUserKey = await this.encryptService.rsaEncrypt(newUserKey.key, publicKey);
|
||||
const newEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||
newUserKey,
|
||||
publicKey,
|
||||
);
|
||||
|
||||
const newRotateableKeySet = new RotateableKeySet<ExternalKey>(
|
||||
newEncryptedUserKey,
|
||||
|
||||
@@ -42,3 +42,7 @@ export class ViewTypeEmergencyAccess {
|
||||
keyEncrypted: string;
|
||||
ciphers: CipherResponse[] = [];
|
||||
}
|
||||
|
||||
export class GranteeEmergencyAccessWithPublicKey extends GranteeEmergencyAccess {
|
||||
publicKey: Uint8Array;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.resp
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
@@ -41,6 +42,9 @@ describe("EmergencyAccessService", () => {
|
||||
let emergencyAccessService: EmergencyAccessService;
|
||||
let configService: ConfigService;
|
||||
|
||||
const mockNewUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("trustedPublicKey")];
|
||||
|
||||
beforeAll(() => {
|
||||
emergencyAccessApiService = mock<EmergencyAccessApiService>();
|
||||
apiService = mock<ApiService>();
|
||||
@@ -126,7 +130,9 @@ describe("EmergencyAccessService", () => {
|
||||
|
||||
keyService.getUserKey.mockResolvedValueOnce(mockUserKey);
|
||||
|
||||
encryptService.rsaEncrypt.mockResolvedValueOnce(mockUserPublicKeyEncryptedUserKey);
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValueOnce(
|
||||
mockUserPublicKeyEncryptedUserKey,
|
||||
);
|
||||
|
||||
emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce();
|
||||
|
||||
@@ -156,7 +162,9 @@ describe("EmergencyAccessService", () => {
|
||||
|
||||
const mockDecryptedGrantorUserKey = new Uint8Array(64);
|
||||
keyService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
|
||||
encryptService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
|
||||
encryptService.decapsulateKeyUnsigned.mockResolvedValueOnce(
|
||||
new SymmetricCryptoKey(mockDecryptedGrantorUserKey),
|
||||
);
|
||||
|
||||
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
|
||||
|
||||
@@ -226,10 +234,6 @@ describe("EmergencyAccessService", () => {
|
||||
});
|
||||
|
||||
describe("getRotatedData", () => {
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockOriginalUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||
const mockNewUserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey;
|
||||
|
||||
const allowedStatuses = [
|
||||
EmergencyAccessStatusType.Confirmed,
|
||||
EmergencyAccessStatusType.RecoveryInitiated,
|
||||
@@ -250,10 +254,10 @@ describe("EmergencyAccessService", () => {
|
||||
emergencyAccessApiService.getEmergencyAccessTrusted.mockResolvedValue(mockEmergencyAccess);
|
||||
apiService.getUserPublicKey.mockResolvedValue({
|
||||
userId: "mockUserId",
|
||||
publicKey: "mockPublicKey",
|
||||
publicKey: Utils.fromUtf8ToB64("trustedPublicKey"),
|
||||
} as UserKeyResponse);
|
||||
|
||||
encryptService.rsaEncrypt.mockImplementation((plainValue, publicKey) => {
|
||||
encryptService.encapsulateKeyUnsigned.mockImplementation((plainValue, publicKey) => {
|
||||
return Promise.resolve(
|
||||
new EncString(EncryptionType.Rsa2048_OaepSha1_B64, "Encrypted: " + plainValue),
|
||||
);
|
||||
@@ -262,17 +266,32 @@ describe("EmergencyAccessService", () => {
|
||||
|
||||
it("Only returns emergency accesses with allowed statuses", async () => {
|
||||
const result = await emergencyAccessService.getRotatedData(
|
||||
mockOriginalUserKey,
|
||||
mockNewUserKey,
|
||||
mockTrustedPublicKeys,
|
||||
"mockUserId" as UserId,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(allowedStatuses.length);
|
||||
});
|
||||
|
||||
it("Throws if emergency access public key is not trusted", async () => {
|
||||
apiService.getUserPublicKey.mockResolvedValue({
|
||||
userId: "mockUserId",
|
||||
publicKey: Utils.fromUtf8ToB64("untrustedPublicKey"),
|
||||
} as UserKeyResponse);
|
||||
|
||||
await expect(
|
||||
emergencyAccessService.getRotatedData(
|
||||
mockNewUserKey,
|
||||
mockTrustedPublicKeys,
|
||||
"mockUserId" as UserId,
|
||||
),
|
||||
).rejects.toThrow("Public key for user is not trusted.");
|
||||
});
|
||||
|
||||
it("throws if new user key is null", async () => {
|
||||
await expect(
|
||||
emergencyAccessService.getRotatedData(mockOriginalUserKey, null, "mockUserId" as UserId),
|
||||
emergencyAccessService.getRotatedData(null, mockTrustedPublicKeys, "mockUserId" as UserId),
|
||||
).rejects.toThrow("New user key is required for rotation.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString, 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 { UserKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -22,14 +21,18 @@ import {
|
||||
Argon2KdfConfig,
|
||||
KdfConfig,
|
||||
PBKDF2KdfConfig,
|
||||
UserKeyRotationDataProvider,
|
||||
KeyService,
|
||||
KdfType,
|
||||
UserKeyRotationKeyRecoveryProvider,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
|
||||
import { EmergencyAccessType } from "../enums/emergency-access-type";
|
||||
import { GranteeEmergencyAccess, GrantorEmergencyAccess } from "../models/emergency-access";
|
||||
import {
|
||||
GranteeEmergencyAccess,
|
||||
GranteeEmergencyAccessWithPublicKey,
|
||||
GrantorEmergencyAccess,
|
||||
} from "../models/emergency-access";
|
||||
import { EmergencyAccessAcceptRequest } from "../request/emergency-access-accept.request";
|
||||
import { EmergencyAccessConfirmRequest } from "../request/emergency-access-confirm.request";
|
||||
import { EmergencyAccessInviteRequest } from "../request/emergency-access-invite.request";
|
||||
@@ -38,12 +41,17 @@ import {
|
||||
EmergencyAccessUpdateRequest,
|
||||
EmergencyAccessWithIdRequest,
|
||||
} from "../request/emergency-access-update.request";
|
||||
import { EmergencyAccessGranteeDetailsResponse } from "../response/emergency-access.response";
|
||||
|
||||
import { EmergencyAccessApiService } from "./emergency-access-api.service";
|
||||
|
||||
@Injectable()
|
||||
export class EmergencyAccessService
|
||||
implements UserKeyRotationDataProvider<EmergencyAccessWithIdRequest>
|
||||
implements
|
||||
UserKeyRotationKeyRecoveryProvider<
|
||||
EmergencyAccessWithIdRequest,
|
||||
GranteeEmergencyAccessWithPublicKey
|
||||
>
|
||||
{
|
||||
constructor(
|
||||
private emergencyAccessApiService: EmergencyAccessApiService,
|
||||
@@ -225,11 +233,10 @@ export class EmergencyAccessService
|
||||
throw new Error("Active user does not have a private key, cannot get view only ciphers.");
|
||||
}
|
||||
|
||||
const grantorKeyBuffer = await this.encryptService.rsaDecrypt(
|
||||
const grantorUserKey = (await this.encryptService.decapsulateKeyUnsigned(
|
||||
new EncString(response.keyEncrypted),
|
||||
activeUserPrivateKey,
|
||||
);
|
||||
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
|
||||
)) as UserKey;
|
||||
|
||||
let ciphers: CipherView[] = [];
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) {
|
||||
@@ -262,15 +269,15 @@ export class EmergencyAccessService
|
||||
throw new Error("Active user does not have a private key, cannot complete a takeover.");
|
||||
}
|
||||
|
||||
const grantorKeyBuffer = await this.encryptService.rsaDecrypt(
|
||||
const grantorKey = await this.encryptService.decapsulateKeyUnsigned(
|
||||
new EncString(takeoverResponse.keyEncrypted),
|
||||
activeUserPrivateKey,
|
||||
);
|
||||
if (grantorKeyBuffer == null) {
|
||||
if (grantorKey == null) {
|
||||
throw new Error("Failed to decrypt grantor key");
|
||||
}
|
||||
|
||||
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
|
||||
const grantorUserKey = grantorKey as UserKey;
|
||||
|
||||
let config: KdfConfig;
|
||||
|
||||
@@ -301,30 +308,12 @@ export class EmergencyAccessService
|
||||
this.emergencyAccessApiService.postEmergencyAccessPassword(id, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns existing emergency access keys re-encrypted with new user key.
|
||||
* Intended for grantor.
|
||||
* @param originalUserKey the original user key
|
||||
* @param newUserKey the new user key
|
||||
* @param userId the user id
|
||||
* @throws Error if newUserKey is nullish
|
||||
* @returns an array of re-encrypted emergency access requests or an empty array if there are no requests
|
||||
*/
|
||||
async getRotatedData(
|
||||
originalUserKey: UserKey,
|
||||
newUserKey: UserKey,
|
||||
userId: UserId,
|
||||
): Promise<EmergencyAccessWithIdRequest[]> {
|
||||
if (newUserKey == null) {
|
||||
throw new Error("New user key is required for rotation.");
|
||||
}
|
||||
|
||||
const requests: EmergencyAccessWithIdRequest[] = [];
|
||||
private async getEmergencyAccessData(): Promise<EmergencyAccessGranteeDetailsResponse[]> {
|
||||
const existingEmergencyAccess =
|
||||
await this.emergencyAccessApiService.getEmergencyAccessTrusted();
|
||||
|
||||
if (!existingEmergencyAccess || existingEmergencyAccess.data.length === 0) {
|
||||
return requests;
|
||||
return [];
|
||||
}
|
||||
|
||||
// Any Invited or Accepted requests won't have the key yet, so we don't need to update them
|
||||
@@ -337,13 +326,73 @@ export class EmergencyAccessService
|
||||
allowedStatuses.has(d.status),
|
||||
);
|
||||
|
||||
for (const details of filteredAccesses) {
|
||||
// Get public key of grantee
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(details.granteeId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
return filteredAccesses;
|
||||
}
|
||||
|
||||
async getPublicKeys(): Promise<GranteeEmergencyAccessWithPublicKey[]> {
|
||||
const emergencyAccessData = await this.getEmergencyAccessData();
|
||||
const emergencyAccessDataWithPublicKeys = await Promise.all(
|
||||
emergencyAccessData.map(async (details) => {
|
||||
const grantee = new GranteeEmergencyAccessWithPublicKey();
|
||||
grantee.id = details.id;
|
||||
grantee.granteeId = details.granteeId;
|
||||
grantee.name = details.name;
|
||||
grantee.email = details.email;
|
||||
grantee.type = details.type;
|
||||
grantee.status = details.status;
|
||||
grantee.waitTimeDays = details.waitTimeDays;
|
||||
grantee.creationDate = details.creationDate;
|
||||
grantee.avatarColor = details.avatarColor;
|
||||
grantee.publicKey = Utils.fromB64ToArray(
|
||||
(await this.apiService.getUserPublicKey(details.granteeId)).publicKey,
|
||||
);
|
||||
return grantee;
|
||||
}),
|
||||
);
|
||||
|
||||
return emergencyAccessDataWithPublicKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns existing emergency access keys re-encrypted with new user key.
|
||||
* Intended for grantor.
|
||||
* @param newUserKey the new user key
|
||||
* @param trustedPublicKeys the public keys of the emergency access grantors. These *must* be trusted somehow, and MUST NOT be passed in untrusted
|
||||
* @param userId the user id
|
||||
* @throws Error if newUserKey is nullish
|
||||
* @returns an array of re-encrypted emergency access requests or an empty array if there are no requests
|
||||
*/
|
||||
async getRotatedData(
|
||||
newUserKey: UserKey,
|
||||
trustedPublicKeys: Uint8Array[],
|
||||
userId: UserId,
|
||||
): Promise<EmergencyAccessWithIdRequest[]> {
|
||||
if (newUserKey == null) {
|
||||
throw new Error("New user key is required for rotation.");
|
||||
}
|
||||
|
||||
const requests: EmergencyAccessWithIdRequest[] = [];
|
||||
|
||||
this.logService.info(
|
||||
"Starting emergency access rotation, with trusted keys: ",
|
||||
trustedPublicKeys,
|
||||
);
|
||||
|
||||
const allDetails = await this.getPublicKeys();
|
||||
for (const details of allDetails) {
|
||||
if (
|
||||
trustedPublicKeys.find(
|
||||
(pk) => Utils.fromBufferToHex(pk) === Utils.fromBufferToHex(details.publicKey),
|
||||
) == null
|
||||
) {
|
||||
this.logService.info(
|
||||
`Public key for user ${details.granteeId} is not trusted, skipping rotation.`,
|
||||
);
|
||||
throw new Error("Public key for user is not trusted.");
|
||||
}
|
||||
|
||||
// Encrypt new user key with public key
|
||||
const encryptedKey = await this.encryptKey(newUserKey, publicKey);
|
||||
const encryptedKey = await this.encryptKey(newUserKey, details.publicKey);
|
||||
|
||||
const updateRequest = new EmergencyAccessWithIdRequest();
|
||||
updateRequest.id = details.id;
|
||||
@@ -356,6 +405,6 @@ export class EmergencyAccessService
|
||||
}
|
||||
|
||||
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
||||
return (await this.encryptService.rsaEncrypt(userKey.key, publicKey)).encryptedString;
|
||||
return (await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey)).encryptedString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -11,14 +12,19 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
|
||||
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { FakeGlobalState } from "@bitwarden/common/spec/fake-state";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component";
|
||||
import { I18nService } from "../../core/i18n.service";
|
||||
|
||||
import {
|
||||
@@ -41,6 +47,8 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let globalStateProvider: FakeGlobalStateProvider;
|
||||
let globalState: FakeGlobalState<OrganizationInvite>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock();
|
||||
@@ -55,6 +63,8 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
i18nService = mock();
|
||||
globalStateProvider = new FakeGlobalStateProvider();
|
||||
globalState = globalStateProvider.getFake(ORGANIZATION_INVITE);
|
||||
dialogService = mock();
|
||||
accountService = mock();
|
||||
|
||||
sut = new AcceptOrganizationInviteService(
|
||||
apiService,
|
||||
@@ -68,6 +78,8 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
organizationUserApiService,
|
||||
i18nService,
|
||||
globalStateProvider,
|
||||
dialogService,
|
||||
accountService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -142,7 +154,7 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
expect(authService.logOut).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts the invitation request when the org has a master password policy, but the user has already passed it", async () => {
|
||||
it("accepts the invitation request when the org has a master password policy, but the user has already passed it and autoenroll is not enabled", async () => {
|
||||
const invite = createOrgInvite();
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue([
|
||||
{
|
||||
@@ -167,6 +179,53 @@ describe("AcceptOrganizationInviteService", () => {
|
||||
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
|
||||
expect(authService.logOut).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("accepts the invitation request and enrolls when autoenroll is enabled", async () => {
|
||||
const invite = createOrgInvite();
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue([
|
||||
{
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: true,
|
||||
} as Policy,
|
||||
]);
|
||||
organizationApiService.getKeys.mockResolvedValue(
|
||||
new OrganizationKeysResponse({
|
||||
privateKey: "privateKey",
|
||||
publicKey: "publicKey",
|
||||
}),
|
||||
);
|
||||
accountService.activeAccount$ = new BehaviorSubject({ id: "activeUserId" }) as any;
|
||||
keyService.userKey$.mockReturnValue(new BehaviorSubject({ key: "userKey" } as any));
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue({
|
||||
encryptedString: "encryptedString",
|
||||
} as EncString);
|
||||
|
||||
jest.mock("../../admin-console/organizations/manage/organization-trust.component");
|
||||
OrganizationTrustComponent.open = jest.fn().mockReturnValue({
|
||||
closed: new BehaviorSubject(true),
|
||||
});
|
||||
|
||||
await globalState.update(() => invite);
|
||||
|
||||
policyService.getResetPasswordPolicyOptions.mockReturnValue([
|
||||
{
|
||||
autoEnrollEnabled: true,
|
||||
} as ResetPasswordPolicyOptions,
|
||||
true,
|
||||
]);
|
||||
|
||||
const result = await sut.validateAndAcceptInvite(invite);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(OrganizationTrustComponent.open).toHaveBeenCalled();
|
||||
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
|
||||
{ key: "userKey" },
|
||||
Utils.fromB64ToArray("publicKey"),
|
||||
);
|
||||
expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled();
|
||||
expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled();
|
||||
expect(authService.logOut).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -27,8 +28,11 @@ import {
|
||||
ORGANIZATION_INVITE_DISK,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component";
|
||||
|
||||
import { OrganizationInvite } from "./organization-invite";
|
||||
|
||||
// We're storing the organization invite for 2 reasons:
|
||||
@@ -63,6 +67,8 @@ export class AcceptOrganizationInviteService {
|
||||
private readonly organizationUserApiService: OrganizationUserApiService,
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly globalStateProvider: GlobalStateProvider,
|
||||
private readonly dialogService: DialogService,
|
||||
private readonly accountService: AccountService,
|
||||
) {
|
||||
this.organizationInvitationState = this.globalStateProvider.get(ORGANIZATION_INVITE);
|
||||
}
|
||||
@@ -183,10 +189,20 @@ export class AcceptOrganizationInviteService {
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromB64ToArray(response.publicKey);
|
||||
const dialogRef = OrganizationTrustComponent.open(this.dialogService, {
|
||||
name: invite.organizationName,
|
||||
orgId: invite.organizationId,
|
||||
publicKey,
|
||||
});
|
||||
const result = await firstValueFrom(dialogRef.closed);
|
||||
if (result !== true) {
|
||||
throw new Error("Organization not trusted, aborting user key rotation");
|
||||
}
|
||||
|
||||
const activeUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId));
|
||||
// RSA Encrypt user's encKey.key with organization public key
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, publicKey);
|
||||
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey);
|
||||
|
||||
// Add reset password key to accept request
|
||||
request.resetPasswordKey = encryptedKey.encryptedString;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
@@ -17,7 +16,13 @@ import { ProfileResponse } from "@bitwarden/common/models/response/profile.respo
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
type ChangeAvatarDialogData = {
|
||||
profile: ProfileResponse;
|
||||
|
||||
@@ -39,10 +39,12 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<button type="submit" bitButton buttonType="primary" bitFormButton>
|
||||
{{ (tokenSent ? "changeEmail" : "continue") | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton *ngIf="tokenSent" (click)="reset()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="submit" bitButton buttonType="primary" bitFormButton>
|
||||
{{ (tokenSent ? "changeEmail" : "continue") | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton *ngIf="tokenSent" (click)="reset()">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import mock, { MockProxy } from "jest-mock-extended/lib/Mock";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorProviderResponse } from "@bitwarden/common/auth/models/response/two-factor-provider.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { ChangeEmailComponent } from "@bitwarden/web-vault/app/auth/settings/account/change-email.component";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
describe("ChangeEmailComponent", () => {
|
||||
let component: ChangeEmailComponent;
|
||||
let fixture: ComponentFixture<ChangeEmailComponent>;
|
||||
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let accountService: FakeAccountService;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
apiService = mock<ApiService>();
|
||||
keyService = mock<KeyService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
accountService = mockAccountServiceWith("UserId" as UserId);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ChangeEmailComponent],
|
||||
imports: [ReactiveFormsModule, SharedModule],
|
||||
providers: [
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: ApiService, useValue: apiService },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: MessagingService, useValue: mock<MessagingService>() },
|
||||
{ provide: KdfConfigService, useValue: kdfConfigService },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: FormBuilder, useClass: FormBuilder },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChangeEmailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("creates component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
beforeEach(() => {
|
||||
apiService.getTwoFactorProviders.mockResolvedValue({
|
||||
data: [{ type: TwoFactorProviderType.Email, enabled: true } as TwoFactorProviderResponse],
|
||||
} as ListResponse<TwoFactorProviderResponse>);
|
||||
});
|
||||
|
||||
it("initializes userId", async () => {
|
||||
await component.ngOnInit();
|
||||
expect(component.userId).toBe("UserId");
|
||||
});
|
||||
|
||||
it("errors if there is no active user", async () => {
|
||||
// clear active account
|
||||
await firstValueFrom(accountService.activeAccount$);
|
||||
accountService.activeAccountSubject.next(null);
|
||||
|
||||
await expect(() => component.ngOnInit()).rejects.toThrow("Null or undefined account");
|
||||
});
|
||||
|
||||
it("initializes showTwoFactorEmailWarning", async () => {
|
||||
await component.ngOnInit();
|
||||
expect(component.showTwoFactorEmailWarning).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("submit", () => {
|
||||
beforeEach(() => {
|
||||
component.formGroup.controls.step1.setValue({
|
||||
masterPassword: "password",
|
||||
newEmail: "test@example.com",
|
||||
});
|
||||
|
||||
keyService.getOrDeriveMasterKey
|
||||
.calledWith("password", "UserId")
|
||||
.mockResolvedValue("getOrDeriveMasterKey" as any);
|
||||
keyService.hashMasterKey
|
||||
.calledWith("password", "getOrDeriveMasterKey" as any)
|
||||
.mockResolvedValue("existingHash");
|
||||
});
|
||||
|
||||
it("throws if userId is null on submit", async () => {
|
||||
component.userId = undefined;
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Can't find user");
|
||||
});
|
||||
|
||||
describe("step 1", () => {
|
||||
it("does not submit if step 1 is invalid", async () => {
|
||||
component.formGroup.controls.step1.setValue({
|
||||
masterPassword: "",
|
||||
newEmail: "",
|
||||
});
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(apiService.postEmailToken).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends email token in step 1 if tokenSent is false", async () => {
|
||||
await component.submit();
|
||||
|
||||
expect(apiService.postEmailToken).toHaveBeenCalledWith({
|
||||
newEmail: "test@example.com",
|
||||
masterPasswordHash: "existingHash",
|
||||
});
|
||||
// should activate step 2
|
||||
expect(component.tokenSent).toBe(true);
|
||||
expect(component.formGroup.controls.step1.disabled).toBe(true);
|
||||
expect(component.formGroup.controls.token.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("step 2", () => {
|
||||
beforeEach(() => {
|
||||
component.tokenSent = true;
|
||||
component.formGroup.controls.step1.disable();
|
||||
component.formGroup.controls.token.enable();
|
||||
component.formGroup.controls.token.setValue("token");
|
||||
|
||||
kdfConfigService.getKdfConfig$
|
||||
.calledWith("UserId" as any)
|
||||
.mockReturnValue(of("kdfConfig" as any));
|
||||
keyService.userKey$.calledWith("UserId" as any).mockReturnValue(of("userKey" as any));
|
||||
|
||||
keyService.makeMasterKey
|
||||
.calledWith("password", "test@example.com", "kdfConfig" as any)
|
||||
.mockResolvedValue("newMasterKey" as any);
|
||||
keyService.hashMasterKey
|
||||
.calledWith("password", "newMasterKey" as any)
|
||||
.mockResolvedValue("newMasterKeyHash");
|
||||
|
||||
// Important: make sure this is called with new master key, not existing
|
||||
keyService.encryptUserKeyWithMasterKey
|
||||
.calledWith("newMasterKey" as any, "userKey" as any)
|
||||
.mockResolvedValue(["userKey" as any, { encryptedString: "newEncryptedUserKey" } as any]);
|
||||
});
|
||||
|
||||
it("does not post email if token is missing on submit", async () => {
|
||||
component.formGroup.controls.token.setValue("");
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(apiService.postEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws if kdfConfig is missing on submit", async () => {
|
||||
kdfConfigService.getKdfConfig$.mockReturnValue(of(null));
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Missing kdf config");
|
||||
});
|
||||
|
||||
it("throws if userKey can't be found", async () => {
|
||||
keyService.userKey$.mockReturnValue(of(null));
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Can't find UserKey");
|
||||
});
|
||||
|
||||
it("throws if encryptedUserKey is missing", async () => {
|
||||
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(["userKey" as any, null as any]);
|
||||
|
||||
await expect(component.submit()).rejects.toThrow("Missing Encrypted User Key");
|
||||
});
|
||||
|
||||
it("submits if step 2 is valid", async () => {
|
||||
await component.submit();
|
||||
|
||||
// validate that hashes are correct
|
||||
expect(apiService.postEmail).toHaveBeenCalledWith({
|
||||
masterPasswordHash: "existingHash",
|
||||
newMasterPasswordHash: "newMasterKeyHash",
|
||||
token: "token",
|
||||
newEmail: "test@example.com",
|
||||
key: "newEncryptedUserKey",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
|
||||
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -22,8 +21,9 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
export class ChangeEmailComponent implements OnInit {
|
||||
tokenSent = false;
|
||||
showTwoFactorEmailWarning = false;
|
||||
userId: UserId | undefined;
|
||||
|
||||
protected formGroup = this.formBuilder.group({
|
||||
formGroup = this.formBuilder.group({
|
||||
step1: this.formBuilder.group({
|
||||
masterPassword: ["", [Validators.required]],
|
||||
newEmail: ["", [Validators.required, Validators.email]],
|
||||
@@ -32,26 +32,30 @@ export class ChangeEmailComponent implements OnInit {
|
||||
});
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private keyService: KeyService,
|
||||
private messagingService: MessagingService,
|
||||
private logService: LogService,
|
||||
private stateService: StateService,
|
||||
private formBuilder: FormBuilder,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
const twoFactorProviders = await this.apiService.getTwoFactorProviders();
|
||||
this.showTwoFactorEmailWarning = twoFactorProviders.data.some(
|
||||
(p) => p.type === TwoFactorProviderType.Email && p.enabled,
|
||||
);
|
||||
}
|
||||
|
||||
protected submit = async () => {
|
||||
submit = async () => {
|
||||
if (this.userId == null) {
|
||||
throw new Error("Can't find user");
|
||||
}
|
||||
|
||||
// This form has multiple steps, so we need to mark all the groups as touched.
|
||||
this.formGroup.controls.step1.markAllAsTouched();
|
||||
|
||||
@@ -65,37 +69,54 @@ export class ChangeEmailComponent implements OnInit {
|
||||
}
|
||||
|
||||
const step1Value = this.formGroup.controls.step1.value;
|
||||
const newEmail = step1Value.newEmail.trim().toLowerCase();
|
||||
const newEmail = step1Value.newEmail?.trim().toLowerCase();
|
||||
const masterPassword = step1Value.masterPassword;
|
||||
|
||||
if (newEmail == null || masterPassword == null) {
|
||||
throw new Error("Missing email or password");
|
||||
}
|
||||
|
||||
const existingHash = await this.keyService.hashMasterKey(
|
||||
masterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(masterPassword, this.userId),
|
||||
);
|
||||
|
||||
if (!this.tokenSent) {
|
||||
const request = new EmailTokenRequest();
|
||||
request.newEmail = newEmail;
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
step1Value.masterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(step1Value.masterPassword),
|
||||
);
|
||||
request.masterPasswordHash = existingHash;
|
||||
await this.apiService.postEmailToken(request);
|
||||
this.activateStep2();
|
||||
} else {
|
||||
const token = this.formGroup.value.token;
|
||||
if (token == null) {
|
||||
throw new Error("Missing token");
|
||||
}
|
||||
const request = new EmailRequest();
|
||||
request.token = this.formGroup.value.token;
|
||||
request.token = token;
|
||||
request.newEmail = newEmail;
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
step1Value.masterPassword,
|
||||
await this.keyService.getOrDeriveMasterKey(step1Value.masterPassword),
|
||||
);
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
step1Value.masterPassword,
|
||||
newEmail,
|
||||
kdfConfig,
|
||||
);
|
||||
request.masterPasswordHash = existingHash;
|
||||
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId));
|
||||
if (kdfConfig == null) {
|
||||
throw new Error("Missing kdf config");
|
||||
}
|
||||
const newMasterKey = await this.keyService.makeMasterKey(masterPassword, newEmail, kdfConfig);
|
||||
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
|
||||
step1Value.masterPassword,
|
||||
masterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
const newUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey);
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(this.userId));
|
||||
if (userKey == null) {
|
||||
throw new Error("Can't find UserKey");
|
||||
}
|
||||
const newUserKey = await this.keyService.encryptUserKeyWithMasterKey(newMasterKey, userKey);
|
||||
const encryptedUserKey = newUserKey[1]?.encryptedString;
|
||||
if (encryptedUserKey == null) {
|
||||
throw new Error("Missing Encrypted User Key");
|
||||
}
|
||||
request.key = encryptedUserKey;
|
||||
|
||||
await this.apiService.postEmail(request);
|
||||
this.reset();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
@@ -8,7 +7,7 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
templateUrl: "delete-account-dialog.component.html",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
@@ -15,6 +14,7 @@ import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
DialogRef,
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
[(ngModel)]="masterPasswordHint"
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="loading">
|
||||
{{ "changeMasterPassword" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -12,7 +12,9 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -35,11 +37,13 @@ export class ChangePasswordComponent
|
||||
extends BaseChangePasswordComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
loading = false;
|
||||
rotateUserKey = false;
|
||||
currentMasterPassword: string;
|
||||
masterPasswordHint: string;
|
||||
checkForBreaches = true;
|
||||
characterMinimumMessage = "";
|
||||
userkeyRotationV2 = false;
|
||||
|
||||
constructor(
|
||||
i18nService: I18nService,
|
||||
@@ -56,9 +60,10 @@ export class ChangePasswordComponent
|
||||
private userVerificationService: UserVerificationService,
|
||||
private keyRotationService: UserKeyRotationService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
@@ -75,6 +80,8 @@ export class ChangePasswordComponent
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.userkeyRotationV2 = await this.configService.getFeatureFlag(FeatureFlag.UserKeyRotationV2);
|
||||
|
||||
if (!(await this.userVerificationService.hasMasterPassword())) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@@ -137,6 +144,130 @@ export class ChangePasswordComponent
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.userkeyRotationV2) {
|
||||
this.loading = true;
|
||||
await this.submitNew();
|
||||
this.loading = false;
|
||||
} else {
|
||||
await this.submitOld();
|
||||
}
|
||||
}
|
||||
|
||||
async submitNew() {
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.masterPasswordHint != null &&
|
||||
this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase()
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("hintEqualsPassword"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.leakedPassword = false;
|
||||
if (this.checkForBreaches) {
|
||||
this.leakedPassword = (await this.auditService.passwordLeaked(this.masterPassword)) > 0;
|
||||
}
|
||||
|
||||
if (!(await this.strongPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.rotateUserKey) {
|
||||
await this.syncService.fullSync(true);
|
||||
const user = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.keyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
this.currentMasterPassword,
|
||||
this.masterPassword,
|
||||
user,
|
||||
this.masterPasswordHint,
|
||||
);
|
||||
} else {
|
||||
await this.updatePassword(this.masterPassword);
|
||||
}
|
||||
} catch (e) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// todo: move this to a service
|
||||
// https://bitwarden.atlassian.net/browse/PM-17108
|
||||
private async updatePassword(newMasterPassword: string) {
|
||||
const currentMasterPassword = this.currentMasterPassword;
|
||||
const { userId, email } = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => ({ userId: a?.id, email: a?.email }))),
|
||||
);
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
|
||||
|
||||
const currentMasterKey = await this.keyService.makeMasterKey(
|
||||
currentMasterPassword,
|
||||
email,
|
||||
kdfConfig,
|
||||
);
|
||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
currentMasterKey,
|
||||
userId,
|
||||
);
|
||||
if (decryptedUserKey == null) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("invalidMasterPassword"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newMasterKey = await this.keyService.makeMasterKey(newMasterPassword, email, kdfConfig);
|
||||
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
newMasterKey,
|
||||
decryptedUserKey,
|
||||
);
|
||||
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.keyService.hashMasterKey(
|
||||
this.currentMasterPassword,
|
||||
currentMasterKey,
|
||||
);
|
||||
request.masterPasswordHint = this.masterPasswordHint;
|
||||
request.newMasterPasswordHash = await this.keyService.hashMasterKey(
|
||||
newMasterPassword,
|
||||
newMasterKey,
|
||||
);
|
||||
request.key = newMasterKeyEncryptedUserKey[1].encryptedString;
|
||||
try {
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("masterPasswordChanged"),
|
||||
message: this.i18nService.t("masterPasswordChangedDesc"),
|
||||
});
|
||||
this.messagingService.send("logout");
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async submitOld() {
|
||||
if (
|
||||
this.masterPasswordHint != null &&
|
||||
this.masterPasswordHint.toLowerCase() === this.masterPassword.toLowerCase()
|
||||
@@ -242,6 +373,6 @@ export class ChangePasswordComponent
|
||||
|
||||
private async updateKey() {
|
||||
const user = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedData(this.masterPassword, user);
|
||||
await this.keyRotationService.rotateUserKeyAndEncryptedDataLegacy(this.masterPassword, user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
selector: "emergency-access-attachments",
|
||||
templateUrl: "../../../../vault/individual-vault/attachments.component.html",
|
||||
})
|
||||
export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponent {
|
||||
viewOnly = true;
|
||||
canAccessAttachments = true;
|
||||
|
||||
constructor(
|
||||
cipherService: CipherService,
|
||||
i18nService: I18nService,
|
||||
keyService: KeyService,
|
||||
encryptService: EncryptService,
|
||||
stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
apiService: ApiService,
|
||||
logService: LogService,
|
||||
fileDownloadService: FileDownloadService,
|
||||
dialogService: DialogService,
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
accountService: AccountService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
i18nService,
|
||||
keyService,
|
||||
encryptService,
|
||||
platformUtilsService,
|
||||
apiService,
|
||||
window,
|
||||
logService,
|
||||
stateService,
|
||||
fileDownloadService,
|
||||
dialogService,
|
||||
billingAccountProfileStateService,
|
||||
accountService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
// Do nothing since cipher is already decoded
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(attachment: AttachmentView) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,13 @@
|
||||
<bit-label> {{ "dontAskFingerprintAgain" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||
<span>{{ "confirm" | i18n }}</span>
|
||||
</button>
|
||||
<button bitButton bitFormButton buttonType="secondary" type="button" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit, Inject } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA, DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
export enum EmergencyAccessConfirmDialogResult {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
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 { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DIALOG_DATA,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { EmergencyAccessService } from "../../emergency-access";
|
||||
import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type";
|
||||
|
||||
@@ -42,13 +42,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, OnDestroy, OnInit, Inject, Input } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { takeUntil } from "rxjs";
|
||||
import { switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DIALOG_DATA,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { KdfType, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { EmergencyAccessService } from "../../../emergency-access";
|
||||
@@ -79,9 +85,12 @@ export class EmergencyAccessTakeoverComponent
|
||||
const policies = await this.emergencyAccessService.getGrantorPolicies(
|
||||
this.params.emergencyAccessId,
|
||||
);
|
||||
this.policyService
|
||||
.masterPasswordPolicyOptions$(policies)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((enforcedPolicyOptions) => (this.enforcedPolicyOptions = enforcedPolicyOptions));
|
||||
}
|
||||
|
||||
|
||||
@@ -38,28 +38,6 @@
|
||||
<br />
|
||||
<small class="tw-text-xs">{{ currentCipher.subTitle }}</small>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<div *ngIf="currentCipher.hasAttachments">
|
||||
<button
|
||||
[bitMenuTriggerFor]="optionsMenu"
|
||||
type="button"
|
||||
buttonType="main"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
<bit-menu #optionsMenu>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
appStopClick
|
||||
(click)="viewAttachments(currentCipher)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
|
||||
{{ "attachments" | i18n }}
|
||||
</button>
|
||||
</bit-menu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { EmergencyAccessId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault";
|
||||
|
||||
import { EmergencyAccessService } from "../../../emergency-access";
|
||||
import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component";
|
||||
|
||||
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
|
||||
|
||||
@@ -20,56 +18,34 @@ import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"
|
||||
})
|
||||
export class EmergencyAccessViewComponent implements OnInit {
|
||||
@ViewChild("attachments", { read: ViewContainerRef, static: true })
|
||||
attachmentsModalRef: ViewContainerRef;
|
||||
|
||||
id: string;
|
||||
id: EmergencyAccessId | null = null;
|
||||
ciphers: CipherView[] = [];
|
||||
loaded = false;
|
||||
|
||||
constructor(
|
||||
private modalService: ModalService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private emergencyAccessService: EmergencyAccessService,
|
||||
private dialogService: DialogService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.route.params.subscribe((qParams) => {
|
||||
if (qParams.id == null) {
|
||||
return this.router.navigate(["settings/emergency-access"]);
|
||||
}
|
||||
async ngOnInit() {
|
||||
const qParams = await firstValueFrom(this.route.params);
|
||||
if (qParams.id == null) {
|
||||
await this.router.navigate(["settings/emergency-access"]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = qParams.id;
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.load();
|
||||
});
|
||||
this.id = qParams.id;
|
||||
this.ciphers = await this.emergencyAccessService.getViewOnlyCiphers(qParams.id);
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async selectCipher(cipher: CipherView) {
|
||||
EmergencyViewDialogComponent.open(this.dialogService, {
|
||||
cipher,
|
||||
emergencyAccessId: this.id!,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.ciphers = await this.emergencyAccessService.getViewOnlyCiphers(this.id);
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
// FIXME PM-17747: This will also need to be replaced with the new AttachmentViewDialog
|
||||
async viewAttachments(cipher: CipherView) {
|
||||
await this.modalService.openViewRef(
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
this.attachmentsModalRef,
|
||||
(comp) => {
|
||||
comp.cipher = cipher;
|
||||
comp.emergencyAccessId = this.id;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{ title }}
|
||||
</span>
|
||||
<div bitDialogContent #dialogContent>
|
||||
<app-cipher-view [cipher]="cipher"></app-cipher-view>
|
||||
<app-cipher-view [emergencyAccessId]="emergencyAccessId" [cipher]="cipher"></app-cipher-view>
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="cancel()">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||
@@ -17,8 +16,9 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { ChangeLoginPasswordService, TaskService } from "@bitwarden/vault";
|
||||
import { TaskService } from "@bitwarden/common/vault/tasks";
|
||||
import { DialogService, DialogRef, DIALOG_DATA } from "@bitwarden/components";
|
||||
import { ChangeLoginPasswordService } from "@bitwarden/vault";
|
||||
|
||||
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
|
||||
|
||||
@@ -55,6 +55,7 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
{ provide: DialogRef, useValue: { close } },
|
||||
{ provide: DIALOG_DATA, useValue: { cipher: mockCipher } },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: TaskService, useValue: mock<TaskService>() },
|
||||
],
|
||||
})
|
||||
.overrideComponent(EmergencyViewDialogComponent, {
|
||||
@@ -71,10 +72,6 @@ describe("EmergencyViewDialogComponent", () => {
|
||||
},
|
||||
add: {
|
||||
providers: [
|
||||
{
|
||||
provide: TaskService,
|
||||
useValue: mock<TaskService>(),
|
||||
},
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{
|
||||
provide: ChangeLoginPasswordService,
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { EmergencyAccessId } from "@bitwarden/common/types/guid";
|
||||
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
|
||||
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogRef,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
} from "@bitwarden/components";
|
||||
import {
|
||||
ChangeLoginPasswordService,
|
||||
CipherViewComponent,
|
||||
DefaultChangeLoginPasswordService,
|
||||
DefaultTaskService,
|
||||
TaskService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { WebViewPasswordHistoryService } from "../../../../vault/services/web-view-password-history.service";
|
||||
@@ -22,6 +26,7 @@ import { WebViewPasswordHistoryService } from "../../../../vault/services/web-vi
|
||||
export interface EmergencyViewDialogParams {
|
||||
/** The cipher being viewed. */
|
||||
cipher: CipherView;
|
||||
emergencyAccessId: EmergencyAccessId;
|
||||
}
|
||||
|
||||
/** Stubbed class, premium upgrade is not applicable for emergency viewing */
|
||||
@@ -39,7 +44,6 @@ class PremiumUpgradePromptNoop implements PremiumUpgradePromptService {
|
||||
providers: [
|
||||
{ provide: ViewPasswordHistoryService, useClass: WebViewPasswordHistoryService },
|
||||
{ provide: PremiumUpgradePromptService, useClass: PremiumUpgradePromptNoop },
|
||||
{ provide: TaskService, useClass: DefaultTaskService },
|
||||
{ provide: ChangeLoginPasswordService, useClass: DefaultChangeLoginPasswordService },
|
||||
],
|
||||
})
|
||||
@@ -62,6 +66,10 @@ export class EmergencyViewDialogComponent {
|
||||
return this.params.cipher;
|
||||
}
|
||||
|
||||
get emergencyAccessId(): EmergencyAccessId {
|
||||
return this.params.emergencyAccessId;
|
||||
}
|
||||
|
||||
cancel = () => {
|
||||
this.dialogRef.close();
|
||||
};
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
</p>
|
||||
</bit-callout>
|
||||
</div>
|
||||
<div bitDialogFooter>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" buttonType="primary" *ngIf="!clientSecret" bitButton bitFormButton>
|
||||
<span>{{ (data.isRotation ? "rotateApiKey" : "viewApiKey") | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton bitDialogClose>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
@@ -8,7 +7,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
|
||||
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
|
||||
import { ApiKeyResponse } from "@bitwarden/common/auth/models/response/api-key.response";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
|
||||
export type ApiKeyDialogData = {
|
||||
keyType: string;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormGroup, FormControl, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
@@ -11,7 +10,7 @@ import { KdfRequest } from "@bitwarden/common/models/request/kdf.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { DIALOG_DATA, ToastService } from "@bitwarden/components";
|
||||
import { KdfConfig, KdfType, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
<p bitTypography="body1">
|
||||
{{ "userApiKeyDesc" | i18n }}
|
||||
</p>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewUserApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateUserApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<button type="button" bitButton buttonType="secondary" (click)="viewUserApiKey()">
|
||||
{{ "viewApiKey" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="rotateUserApiKey()">
|
||||
{{ "rotateApiKey" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -15,11 +15,6 @@ import { ApiKeyComponent } from "./api-key.component";
|
||||
templateUrl: "security-keys.component.html",
|
||||
})
|
||||
export class SecurityKeysComponent implements OnInit {
|
||||
@ViewChild("viewUserApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
viewUserApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild("rotateUserApiKeyTemplate", { read: ViewContainerRef, static: true })
|
||||
rotateUserApiKeyModalRef: ViewContainerRef;
|
||||
|
||||
showChangeKdf = true;
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -1,37 +1,50 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { TwoFactorRecoverResponse } from "@bitwarden/common/auth/models/response/two-factor-recover.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
ButtonModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-recovery",
|
||||
templateUrl: "two-factor-recovery.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, DialogModule, ButtonModule, TypographyModule, I18nPipe],
|
||||
})
|
||||
export class TwoFactorRecoveryComponent {
|
||||
type = -1;
|
||||
code: string;
|
||||
authed: boolean;
|
||||
code: string = "";
|
||||
authed: boolean = false;
|
||||
twoFactorProviderType = TwoFactorProviderType;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: any,
|
||||
@Inject(DIALOG_DATA) protected data: { response: { response: TwoFactorRecoverResponse } },
|
||||
private i18nService: I18nService,
|
||||
) {
|
||||
this.auth(data.response);
|
||||
}
|
||||
|
||||
auth(authResponse: any) {
|
||||
auth(authResponse: { response: TwoFactorRecoverResponse }) {
|
||||
this.authed = true;
|
||||
this.processResponse(authResponse.response);
|
||||
}
|
||||
|
||||
print() {
|
||||
const w = window.open();
|
||||
if (!w) {
|
||||
// return early if the window is not open
|
||||
return;
|
||||
}
|
||||
w.document.write(
|
||||
'<div style="font-size: 18px; text-align: center;">' +
|
||||
"<p>" +
|
||||
@@ -48,9 +61,9 @@ export class TwoFactorRecoveryComponent {
|
||||
w.print();
|
||||
}
|
||||
|
||||
private formatString(s: string) {
|
||||
private formatString(s: string): string {
|
||||
if (s == null) {
|
||||
return null;
|
||||
return "";
|
||||
}
|
||||
return s
|
||||
.replace(/(.{4})/g, "$1 ")
|
||||
@@ -62,7 +75,13 @@ export class TwoFactorRecoveryComponent {
|
||||
this.code = this.formatString(response.code);
|
||||
}
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<any>) {
|
||||
static open(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<
|
||||
{ response: { response: TwoFactorRecoverResponse } },
|
||||
DialogRef<unknown, TwoFactorRecoveryComponent>
|
||||
>,
|
||||
) {
|
||||
return dialogService.open(TwoFactorRecoveryComponent, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
{{ "twoStepAuthenticatorInstructionSuffix" | i18n }}
|
||||
</p>
|
||||
|
||||
<p class="text-center">
|
||||
<p class="tw-text-center">
|
||||
<a
|
||||
href="https://apps.apple.com/ca/app/bitwarden-authenticator/id6497335175"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
@@ -18,7 +19,23 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
LinkModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
|
||||
|
||||
@@ -39,6 +56,22 @@ declare global {
|
||||
@Component({
|
||||
selector: "app-two-factor-setup-authenticator",
|
||||
templateUrl: "two-factor-setup-authenticator.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
InputModule,
|
||||
LinkModule,
|
||||
TypographyModule,
|
||||
CalloutModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
I18nPipe,
|
||||
AsyncActionsModule,
|
||||
JslibModule,
|
||||
],
|
||||
})
|
||||
export class TwoFactorSetupAuthenticatorComponent
|
||||
extends TwoFactorSetupMethodBaseComponent
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<bit-dialog [title]="'twoStepLogin' | i18n" [subtitle]="'Duo'">
|
||||
<ng-container bitDialogContent>
|
||||
<ng-container *ngIf="enabled">
|
||||
<app-callout type="success" title="{{ 'enabled' | i18n }}" icon="bwi bwi-check-circle">
|
||||
<bit-callout type="success" [title]="'enabled' | i18n" icon="bwi bwi-check-circle">
|
||||
{{ "twoStepLoginProviderEnabled" | i18n }}
|
||||
</app-callout>
|
||||
</bit-callout>
|
||||
<img class="tw-float-right tw-ml-3 mfaType2" alt="Duo logo" />
|
||||
<strong>{{ "twoFactorDuoClientId" | i18n }}:</strong> {{ clientId }}
|
||||
<br />
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
@@ -13,13 +11,42 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
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 { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-setup-duo",
|
||||
templateUrl: "two-factor-setup-duo.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
InputModule,
|
||||
TypographyModule,
|
||||
ButtonModule,
|
||||
IconModule,
|
||||
I18nPipe,
|
||||
ReactiveFormsModule,
|
||||
AsyncActionsModule,
|
||||
CalloutModule,
|
||||
],
|
||||
})
|
||||
export class TwoFactorSetupDuoComponent
|
||||
extends TwoFactorSetupMethodBaseComponent
|
||||
@@ -58,23 +85,23 @@ export class TwoFactorSetupDuoComponent
|
||||
);
|
||||
}
|
||||
|
||||
get clientId() {
|
||||
return this.formGroup.get("clientId").value;
|
||||
get clientId(): string {
|
||||
return this.formGroup.get("clientId")?.value || "";
|
||||
}
|
||||
get clientSecret() {
|
||||
return this.formGroup.get("clientSecret").value;
|
||||
get clientSecret(): string {
|
||||
return this.formGroup.get("clientSecret")?.value || "";
|
||||
}
|
||||
get host() {
|
||||
return this.formGroup.get("host").value;
|
||||
get host(): string {
|
||||
return this.formGroup.get("host")?.value || "";
|
||||
}
|
||||
set clientId(value: string) {
|
||||
this.formGroup.get("clientId").setValue(value);
|
||||
this.formGroup.get("clientId")?.setValue(value);
|
||||
}
|
||||
set clientSecret(value: string) {
|
||||
this.formGroup.get("clientSecret").setValue(value);
|
||||
this.formGroup.get("clientSecret")?.setValue(value);
|
||||
}
|
||||
set host(value: string) {
|
||||
this.formGroup.get("host").setValue(value);
|
||||
this.formGroup.get("host")?.setValue(value);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -142,7 +169,10 @@ export class TwoFactorSetupDuoComponent
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<TwoFactorDuoComponentConfig>,
|
||||
) => {
|
||||
return dialogService.open<boolean>(TwoFactorSetupDuoComponent, config);
|
||||
return dialogService.open<boolean, TwoFactorDuoComponentConfig>(
|
||||
TwoFactorSetupDuoComponent,
|
||||
config as DialogConfig<TwoFactorDuoComponentConfig, DialogRef<boolean>>,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -16,14 +14,42 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
|
||||
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 { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
InputModule,
|
||||
ToastService,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-setup-email",
|
||||
templateUrl: "two-factor-setup-email.component.html",
|
||||
outputs: ["onUpdated"],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
FormFieldModule,
|
||||
IconModule,
|
||||
I18nPipe,
|
||||
InputModule,
|
||||
ReactiveFormsModule,
|
||||
TypographyModule,
|
||||
],
|
||||
})
|
||||
export class TwoFactorSetupEmailComponent
|
||||
extends TwoFactorSetupMethodBaseComponent
|
||||
@@ -31,8 +57,8 @@ export class TwoFactorSetupEmailComponent
|
||||
{
|
||||
@Output() onChangeStatus: EventEmitter<boolean> = new EventEmitter();
|
||||
type = TwoFactorProviderType.Email;
|
||||
sentEmail: string;
|
||||
emailPromise: Promise<unknown>;
|
||||
sentEmail: string = "";
|
||||
emailPromise: Promise<unknown> | undefined;
|
||||
override componentName = "app-two-factor-email";
|
||||
formGroup = this.formBuilder.group({
|
||||
token: ["", [Validators.required]],
|
||||
@@ -62,17 +88,17 @@ export class TwoFactorSetupEmailComponent
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
get token() {
|
||||
return this.formGroup.get("token").value;
|
||||
get token(): string {
|
||||
return this.formGroup.get("token")?.value || "";
|
||||
}
|
||||
set token(value: string) {
|
||||
this.formGroup.get("token").setValue(value);
|
||||
set token(value: string | null) {
|
||||
this.formGroup.get("token")?.setValue(value || "");
|
||||
}
|
||||
get email() {
|
||||
return this.formGroup.get("email").value;
|
||||
get email(): string {
|
||||
return this.formGroup.get("email")?.value || "";
|
||||
}
|
||||
set email(value: string) {
|
||||
this.formGroup.get("email").setValue(value);
|
||||
set email(value: string | null | undefined) {
|
||||
this.formGroup.get("email")?.setValue(value || "");
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -144,6 +170,9 @@ export class TwoFactorSetupEmailComponent
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AuthResponse<TwoFactorEmailResponse>>,
|
||||
) {
|
||||
return dialogService.open<boolean>(TwoFactorSetupEmailComponent, config);
|
||||
return dialogService.open<boolean, AuthResponse<TwoFactorEmailResponse>>(
|
||||
TwoFactorSetupEmailComponent,
|
||||
config as DialogConfig<AuthResponse<TwoFactorEmailResponse>, DialogRef<boolean>>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -17,18 +15,20 @@ import { DialogService, ToastService } from "@bitwarden/components";
|
||||
/**
|
||||
* Base class for two-factor setup components (ex: email, yubikey, webauthn, duo).
|
||||
*/
|
||||
@Directive()
|
||||
@Directive({
|
||||
standalone: true,
|
||||
})
|
||||
export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
@Output() onUpdated = new EventEmitter<boolean>();
|
||||
|
||||
type: TwoFactorProviderType;
|
||||
organizationId: string;
|
||||
type: TwoFactorProviderType | undefined;
|
||||
organizationId: string | null = null;
|
||||
twoFactorProviderType = TwoFactorProviderType;
|
||||
enabled = false;
|
||||
authed = false;
|
||||
|
||||
protected hashedSecret: string;
|
||||
protected verificationType: VerificationType;
|
||||
protected hashedSecret: string | undefined;
|
||||
protected verificationType: VerificationType | undefined;
|
||||
protected componentName = "";
|
||||
|
||||
constructor(
|
||||
@@ -74,6 +74,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
|
||||
try {
|
||||
const request = await this.buildRequestModel(TwoFactorProviderRequest);
|
||||
if (this.type === undefined) {
|
||||
throw new Error("Two-factor provider type is required");
|
||||
}
|
||||
request.type = this.type;
|
||||
if (this.organizationId != null) {
|
||||
promise = this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
|
||||
@@ -84,7 +87,7 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
this.enabled = false;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("twoStepDisabled"),
|
||||
});
|
||||
this.onUpdated.emit(false);
|
||||
@@ -105,6 +108,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
}
|
||||
|
||||
const request = await this.buildRequestModel(TwoFactorProviderRequest);
|
||||
if (this.type === undefined) {
|
||||
throw new Error("Two-factor provider type is required");
|
||||
}
|
||||
request.type = this.type;
|
||||
if (this.organizationId != null) {
|
||||
await this.apiService.putTwoFactorOrganizationDisable(this.organizationId, request);
|
||||
@@ -114,7 +120,7 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
this.enabled = false;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
title: "",
|
||||
message: this.i18nService.t("twoStepDisabled"),
|
||||
});
|
||||
this.onUpdated.emit(false);
|
||||
@@ -123,6 +129,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
|
||||
protected async buildRequestModel<T extends SecretVerificationRequest>(
|
||||
requestClass: new () => T,
|
||||
) {
|
||||
if (this.hashedSecret === undefined || this.verificationType === undefined) {
|
||||
throw new Error("User verification data is missing");
|
||||
}
|
||||
return this.userVerificationService.buildRequest(
|
||||
{
|
||||
secret: this.hashedSecret,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user