1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-14 15:33:55 +00:00

Merge branch 'main' into PM-15090

This commit is contained in:
jaasen-livefront
2024-12-04 14:35:40 -08:00
594 changed files with 24515 additions and 6816 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2024.11.0",
"version": "2024.12.0",
"scripts": {
"build:oss": "webpack",
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@@ -84,12 +84,12 @@
<bit-nav-item
[text]="'importData' | i18n"
route="settings/tools/import"
*ngIf="organization.canAccessImportExport"
*ngIf="organization.canAccessImport"
></bit-nav-item>
<bit-nav-item
[text]="'exportVault' | i18n"
route="settings/tools/export"
*ngIf="organization.canAccessImportExport"
*ngIf="canAccessExport$ | async"
></bit-nav-item>
<bit-nav-item
[text]="'domainVerification' | i18n"

View File

@@ -1,7 +1,7 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, RouterModule } from "@angular/router";
import { combineLatest, map, mergeMap, Observable, Subject, switchMap, takeUntil } from "rxjs";
import { combineLatest, filter, map, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
@@ -12,7 +12,6 @@ import {
canAccessReportingTab,
canAccessSettingsTab,
canAccessVaultTab,
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -22,6 +21,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { getById } from "@bitwarden/common/platform/misc";
import { BannerModule, IconModule } from "@bitwarden/components";
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
@@ -42,19 +42,18 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo";
BannerModule,
],
})
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
export class OrganizationLayoutComponent implements OnInit {
protected readonly logo = AdminConsoleLogo;
protected orgFilter = (org: Organization) => canAccessOrgAdmin(org);
organization$: Observable<Organization>;
canAccessExport$: Observable<boolean>;
showPaymentAndHistory$: Observable<boolean>;
hideNewOrgButton$: Observable<boolean>;
organizationIsUnmanaged$: Observable<boolean>;
isAccessIntelligenceFeatureEnabled = false;
private _destroy = new Subject<void>();
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
@@ -71,23 +70,23 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
FeatureFlag.AccessIntelligence,
);
this.organization$ = this.route.params
.pipe(takeUntil(this._destroy))
.pipe<string>(map((p) => p.organizationId))
.pipe(
mergeMap((id) => {
return this.organizationService.organizations$
.pipe(takeUntil(this._destroy))
.pipe(getOrganizationById(id));
}),
);
this.organization$ = this.route.params.pipe(
map((p) => p.organizationId),
switchMap((id) => this.organizationService.organizations$.pipe(getById(id))),
filter((org) => org != null),
);
this.canAccessExport$ = combineLatest([
this.organization$,
this.configService.getFeatureFlag$(FeatureFlag.PM11360RemoveProviderExportPermission),
]).pipe(map(([org, removeProviderExport]) => org.canAccessExport(removeProviderExport)));
this.showPaymentAndHistory$ = this.organization$.pipe(
map(
(org) =>
!this.platformUtilsService.isSelfHost() &&
org?.canViewBillingHistory &&
org?.canEditPaymentMethods,
org.canViewBillingHistory &&
org.canEditPaymentMethods,
),
);
@@ -107,11 +106,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
);
}
ngOnDestroy() {
this._destroy.next();
this._destroy.complete();
}
canShowVaultTab(organization: Organization): boolean {
return canAccessVaultTab(organization);
}

View File

@@ -8,7 +8,7 @@
</bit-callout>
<ng-container *ngIf="!done">
<bit-callout type="warning" *ngIf="users.length > 0 && !error">
<p bitTypography="body1">{{ "deleteOrganizationUserWarning" | i18n }}</p>
<p bitTypography="body1">{{ "deleteManyOrganizationUsersWarningDesc" | i18n }}</p>
</bit-callout>
<bit-table>
<ng-container header>

View File

@@ -1,4 +1,4 @@
<bit-dialog dialogSize="large" [title]="'removeUsers' | i18n">
<bit-dialog dialogSize="large" [title]="'removeMembers' | i18n">
<ng-container bitDialogContent>
<bit-callout type="danger" *ngIf="users.length <= 0">
{{ "noSelectedUsersApplicable" | i18n }}
@@ -79,7 +79,7 @@
[disabled]="loading"
[bitAction]="submit"
>
{{ "removeUsers" | i18n }}
{{ "removeMembers" | i18n }}
</button>
<button bitButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}

View File

@@ -1,7 +1,14 @@
<bit-dialog>
<bit-dialog
dialogSize="large"
*ngIf="{ enabled: accountDeprovisioningEnabled$ | async } as accountDeprovisioning"
>
<ng-container bitDialogTitle>
<h1>{{ bulkTitle }}</h1>
<h1 *ngIf="accountDeprovisioning.enabled; else nonMemberTitle">{{ bulkMemberTitle }}</h1>
<ng-template #nonMemberTitle>
<h1>{{ bulkTitle }}</h1>
</ng-template>
</ng-container>
<div bitDialogContent>
<bit-callout type="danger" *ngIf="users.length <= 0">
{{ "noSelectedUsersApplicable" | i18n }}
@@ -11,15 +18,81 @@
{{ error }}
</bit-callout>
<ng-container *ngIf="!done">
<bit-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking">
<p>{{ "revokeUsersWarning" | i18n }}</p>
<p *ngIf="this.showNoMasterPasswordWarning">
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
</p>
</bit-callout>
<bit-callout
type="danger"
*ngIf="nonCompliantMembers && accountDeprovisioning.enabled"
title="{{ 'nonCompliantMembersTitle' | i18n }}"
>
{{ "nonCompliantMembersError" | i18n }}
</bit-callout>
<bit-table>
<ng-container *ngIf="!done">
<ng-container *ngIf="accountDeprovisioning.enabled">
<div *ngIf="users.length > 0 && !error && isRevoking">
<p>{{ "revokeMembersWarning" | i18n }}</p>
<ul>
<li>
{{ "claimedAccountRevoke" | i18n }}
</li>
<li>
{{ "unclaimedAccountRevoke" | i18n }}
</li>
</ul>
<p>
{{ "restoreMembersInstructions" | i18n }}
</p>
<p *ngIf="this.showNoMasterPasswordWarning">
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
</p>
</div>
</ng-container>
<ng-container *ngIf="!accountDeprovisioning.enabled">
<bit-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking">
<p>{{ "revokeUsersWarning" | i18n }}</p>
</bit-callout>
</ng-container>
<bit-table *ngIf="accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell class="tw-w-1/2">{{ "member" | i18n }}</th>
<th bitCell *ngIf="isRevoking">{{ "details" | i18n }}</th>
<th bitCell *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<div>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</div>
</div>
</td>
<td *ngIf="isRevoking" bitCell width="30">
{{
user.managedByOrganization ? ("claimedAccount" | i18n) : ("unclaimedAccount" | i18n)
}}
</td>
<td bitCell *ngIf="this.showNoMasterPasswordWarning">
<span class="tw-block tw-lowercase tw-text-muted">
<ng-container *ngIf="user.hasMasterPassword === true"> - </ng-container>
<ng-container *ngIf="user.hasMasterPassword === false">
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i>
{{ "noMasterPassword" | i18n }}
</ng-container>
</span>
</td>
</tr>
</ng-template>
</bit-table>
<bit-table *ngIf="!accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell colspan="2">{{ "user" | i18n }}</th>
@@ -50,21 +123,55 @@
</ng-container>
<ng-container *ngIf="done">
<bit-table>
<bit-table *ngIf="accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell colspan="2">{{ "user" | i18n }}</th>
<th>{{ "status" | i18n }}</th>
<th bitCell class="tw-w-1/2">{{ "member" | i18n }}</th>
<th bitCell>{{ "status" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<div>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</div>
</div>
</td>
<td bitCell>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
<td bitCell *ngIf="statuses.has(user.id)">
{{ statuses.get(user.id) }}
</td>
<td bitCell *ngIf="!statuses.has(user.id)">
{{ "bulkFilteredMessage" | i18n }}
</td>
</tr>
</ng-template>
</bit-table>
<bit-table *ngIf="!accountDeprovisioning.enabled">
<ng-container header>
<tr>
<th bitCell class="tw-w-1/2">{{ "member" | i18n }}</th>
<th bitCell>{{ "status" | i18n }}</th>
</tr>
</ng-container>
<ng-template body>
<tr bitRow *ngFor="let user of users">
<td bitCell width="30">
<div class="tw-flex tw-items-center">
<div class="tw-flex tw-items-center tw-mr-6">
<bit-avatar [text]="user | userName" [id]="user.id" size="small"></bit-avatar>
</div>
<div>
{{ user.email }}
<small class="tw-block tw-text-muted" *ngIf="user.name">{{ user.name }}</small>
</div>
</div>
</td>
<td bitCell *ngIf="statuses.has(user.id)">
{{ statuses.get(user.id) }}
@@ -79,7 +186,7 @@
</div>
<ng-container bitDialogFooter>
<button type="button" bitButton *ngIf="!done && users.length > 0" [bitAction]="submit">
{{ bulkTitle }}
{{ accountDeprovisioning.enabled ? bulkMemberTitle : bulkTitle }}
</button>
<button type="button" bitButton buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}

View File

@@ -1,8 +1,11 @@
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { Observable } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/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 } from "@bitwarden/components";
@@ -29,10 +32,13 @@ export class BulkRestoreRevokeComponent {
done = false;
error: string;
showNoMasterPasswordWarning = false;
nonCompliantMembers: boolean = false;
accountDeprovisioningEnabled$: Observable<boolean>;
constructor(
protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService,
private configService: ConfigService,
@Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams,
) {
this.isRevoking = data.isRevoking;
@@ -41,6 +47,9 @@ export class BulkRestoreRevokeComponent {
this.showNoMasterPasswordWarning = this.users.some(
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
);
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
}
get bulkTitle() {
@@ -48,14 +57,26 @@ export class BulkRestoreRevokeComponent {
return this.i18nService.t(titleKey);
}
get bulkMemberTitle() {
const titleKey = this.isRevoking ? "revokeMembers" : "restoreMembers";
return this.i18nService.t(titleKey);
}
submit = async () => {
try {
const response = await this.performBulkUserAction();
const bulkMessage = this.isRevoking ? "bulkRevokedMessage" : "bulkRestoredMessage";
response.data.forEach((entry) => {
const error = entry.error !== "" ? entry.error : this.i18nService.t(bulkMessage);
response.data.forEach(async (entry) => {
const error =
entry.error !== ""
? this.i18nService.t("cannotRestoreAccessError")
: this.i18nService.t(bulkMessage);
this.statuses.set(entry.id, error);
if (entry.error !== "") {
this.nonCompliantMembers = true;
}
});
this.done = true;
} catch (e) {

View File

@@ -21,6 +21,7 @@ export interface BulkUserDetails {
email: string;
status: OrganizationUserStatusType | ProviderUserStatusType;
hasMasterPassword?: boolean;
managedByOrganization?: boolean;
}
type BulkStatusEntry = {

View File

@@ -579,7 +579,10 @@ export class MemberDialogComponent implements OnDestroy {
key: "deleteOrganizationUser",
placeholders: [this.params.name],
},
content: { key: "deleteOrganizationUserWarning" },
content: {
key: "deleteOrganizationUserWarningDesc",
placeholders: [this.params.name],
},
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },

View File

@@ -123,25 +123,40 @@
{{ "confirmSelected" | i18n }}
</span>
</button>
<button type="button" bitMenuItem (click)="bulkRestore()">
<button
type="button"
bitMenuItem
(click)="bulkRestore()"
*ngIf="showBulkRestoreUsers"
>
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
{{ "restoreAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="bulkRevoke()">
<button
type="button"
bitMenuItem
(click)="bulkRevoke()"
*ngIf="showBulkRevokeUsers"
>
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
{{ "revokeAccess" | i18n }}
</button>
<button type="button" bitMenuItem (click)="bulkRemove()">
<button
type="button"
bitMenuItem
(click)="bulkRemove()"
*ngIf="showBulkRemoveUsers"
>
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i>
{{ "remove" | i18n }}
</span>
</button>
<button
*ngIf="accountDeprovisioningEnabled$ | async"
type="button"
bitMenuItem
(click)="bulkDelete()"
*ngIf="showBulkDeleteUsers"
>
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-trash"></i>
@@ -327,7 +342,7 @@
{{ "revokeAccess" | i18n }}
</button>
<button
*ngIf="!(accountDeprovisioningEnabled$ | async) || !u.managedByOrganization"
*ngIf="!accountDeprovisioningEnabled || !u.managedByOrganization"
type="button"
bitMenuItem
(click)="remove(u)"
@@ -337,7 +352,7 @@
</span>
</button>
<button
*ngIf="(accountDeprovisioningEnabled$ | async) && u.managedByOrganization"
*ngIf="accountDeprovisioningEnabled && u.managedByOrganization"
type="button"
bitMenuItem
(click)="deleteUser(u)"

View File

@@ -1,4 +1,4 @@
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import {
@@ -83,7 +83,7 @@ class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView>
@Component({
templateUrl: "members.component.html",
})
export class MembersComponent extends BaseMembersComponent<OrganizationUserView> {
export class MembersComponent extends BaseMembersComponent<OrganizationUserView> implements OnInit {
@ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true })
resetPasswordModalRef: ViewContainerRef;
@@ -96,13 +96,10 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
status: OrganizationUserStatusType = null;
orgResetPasswordPolicyEnabled = false;
orgIsOnSecretsManagerStandalone = false;
accountDeprovisioningEnabled = false;
protected canUseSecretsManager$: Observable<boolean>;
protected accountDeprovisioningEnabled$: Observable<boolean> = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
// Fixed sizes used for cdkVirtualScroll
protected rowHeight = 69;
protected rowHeightClass = `tw-h-[69px]`;
@@ -216,6 +213,12 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
.subscribe();
}
async ngOnInit() {
this.accountDeprovisioningEnabled = await this.configService.getFeatureFlag(
FeatureFlag.AccountDeprovisioning,
);
}
async getUsers(): Promise<OrganizationUserView[]> {
let groupsPromise: Promise<Map<string, string>>;
let collectionsPromise: Promise<Map<string, string>>;
@@ -739,7 +742,10 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
key: "deleteOrganizationUser",
placeholders: [this.userNamePipe.transform(user)],
},
content: { key: "deleteOrganizationUserWarning" },
content: {
key: "deleteOrganizationUserWarningDesc",
placeholders: [this.userNamePipe.transform(user)],
},
type: "warning",
acceptButtonText: { key: "delete" },
cancelButtonText: { key: "cancel" },
@@ -779,4 +785,65 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
type: "warning",
});
}
get showBulkConfirmUsers(): boolean {
if (!this.accountDeprovisioningEnabled) {
return super.showBulkConfirmUsers;
}
return this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Accepted);
}
get showBulkReinviteUsers(): boolean {
if (!this.accountDeprovisioningEnabled) {
return super.showBulkReinviteUsers;
}
return this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Invited);
}
get showBulkRestoreUsers(): boolean {
return (
!this.accountDeprovisioningEnabled ||
this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Revoked)
);
}
get showBulkRevokeUsers(): boolean {
return (
!this.accountDeprovisioningEnabled ||
this.dataSource
.getCheckedUsers()
.every((member) => member.status != this.userStatusType.Revoked)
);
}
get showBulkRemoveUsers(): boolean {
return (
!this.accountDeprovisioningEnabled ||
this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization)
);
}
get showBulkDeleteUsers(): boolean {
if (!this.accountDeprovisioningEnabled) {
return false;
}
const validStatuses = [
this.userStatusType.Accepted,
this.userStatusType.Confirmed,
this.userStatusType.Revoked,
];
return this.dataSource
.getCheckedUsers()
.every((member) => member.managedByOrganization && validStatuses.includes(member.status));
}
}

View File

@@ -10,13 +10,13 @@ import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { EncryptionType, KdfType } from "@bitwarden/common/platform/enums";
import { EncryptionType } from "@bitwarden/common/platform/enums";
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";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey, OrgKey, MasterKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
import { KdfType, KeyService } from "@bitwarden/key-management";
import { OrganizationUserResetPasswordService } from "./organization-user-reset-password.service";

View File

@@ -7,20 +7,21 @@ import {
} from "@bitwarden/admin-console/common";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import {
Argon2KdfConfig,
KdfConfig,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { KdfType } from "@bitwarden/common/platform/enums";
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 { UserKeyRotationDataProvider, KeyService } from "@bitwarden/key-management";
import {
Argon2KdfConfig,
KdfConfig,
PBKDF2KdfConfig,
UserKeyRotationDataProvider,
KeyService,
KdfType,
} from "@bitwarden/key-management";
@Injectable({
providedIn: "root",

View File

@@ -62,13 +62,6 @@ const routes: Routes = [
(m) => m.OrganizationReportingModule,
),
},
{
path: "access-intelligence",
loadChildren: () =>
import("../../tools/access-intelligence/access-intelligence.module").then(
(m) => m.AccessIntelligenceModule,
),
},
{
path: "billing",
loadChildren: () =>

View File

@@ -45,7 +45,7 @@ export abstract class BasePolicyComponent implements OnInit {
return null;
}
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>) {
buildRequest() {
const request = new PolicyRequest();
request.enabled = this.enabled.value;
request.type = this.policy.type;

View File

@@ -87,7 +87,6 @@ export class PoliciesComponent implements OnInit {
data: {
policy: policy,
organizationId: this.organizationId,
policiesEnabledMap: this.policiesEnabledMap,
},
});

View File

@@ -15,7 +15,13 @@
</div>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary" [disabled]="saveDisabled" bitFormButton type="submit">
<button
bitButton
buttonType="primary"
[disabled]="saveDisabled$ | async"
bitFormButton
type="submit"
>
{{ "save" | i18n }}
</button>
<button bitButton buttonType="secondary" bitDialogClose type="button">

View File

@@ -8,13 +8,13 @@ import {
ViewContainerRef,
} from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Observable, map } from "rxjs";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
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 { BasePolicy, BasePolicyComponent } from "../policies";
@@ -24,8 +24,6 @@ export type PolicyEditDialogData = {
policy: BasePolicy;
/** Returns a unique organization id */
organizationId: string;
/** A map indicating whether each policy type is enabled or disabled. */
policiesEnabledMap: Map<PolicyType, boolean>;
};
export enum PolicyEditDialogResult {
@@ -42,7 +40,7 @@ export class PolicyEditComponent implements AfterViewInit {
policyType = PolicyType;
loading = true;
enabled = false;
saveDisabled = false;
saveDisabled$: Observable<boolean>;
defaultTypes: any[];
policyComponent: BasePolicyComponent;
@@ -54,7 +52,6 @@ export class PolicyEditComponent implements AfterViewInit {
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
private policyApiService: PolicyApiServiceAbstraction,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private cdr: ChangeDetectorRef,
private formBuilder: FormBuilder,
private dialogRef: DialogRef<PolicyEditDialogResult>,
@@ -73,7 +70,9 @@ export class PolicyEditComponent implements AfterViewInit {
this.policyComponent.policy = this.data.policy;
this.policyComponent.policyResponse = this.policyResponse;
this.saveDisabled = !this.policyResponse.canToggleState;
this.saveDisabled$ = this.policyComponent.data.statusChanges.pipe(
map((status) => status !== "VALID" || !this.policyResponse.canToggleState),
);
this.cdr.detectChanges();
}
@@ -96,7 +95,7 @@ export class PolicyEditComponent implements AfterViewInit {
submit = async () => {
let request: PolicyRequest;
try {
request = await this.policyComponent.buildRequest(this.data.policiesEnabledMap);
request = await this.policyComponent.buildRequest();
} catch (e) {
this.toastService.showToast({ variant: "error", title: null, message: e.message });
return;

View File

@@ -2,10 +2,6 @@ import { Component } from "@angular/core";
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 { 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 { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -24,25 +20,4 @@ export class RequireSsoPolicy extends BasePolicy {
selector: "policy-require-sso",
templateUrl: "require-sso.component.html",
})
export class RequireSsoPolicyComponent extends BasePolicyComponent {
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {
super();
}
async buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
if (await this.configService.getFeatureFlag(FeatureFlag.Pm13322AddPolicyDefinitions)) {
// We are now relying on server-side validation only
return super.buildRequest(policiesEnabledMap);
}
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
if (this.enabled.value && !singleOrgEnabled) {
throw new Error(this.i18nService.t("requireSsoPolicyReqError"));
}
return super.buildRequest(policiesEnabledMap);
}
}
export class RequireSsoPolicyComponent extends BasePolicyComponent {}

View File

@@ -2,10 +2,8 @@ import { Component, OnInit } from "@angular/core";
import { firstValueFrom, Observable } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
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 { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@@ -21,10 +19,7 @@ export class SingleOrgPolicy extends BasePolicy {
templateUrl: "single-org.component.html",
})
export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnInit {
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {
constructor(private configService: ConfigService) {
super();
}
@@ -44,39 +39,4 @@ export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnI
this.enabled.disable();
}
}
async buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
if (await this.configService.getFeatureFlag(FeatureFlag.Pm13322AddPolicyDefinitions)) {
// We are now relying on server-side validation only
return super.buildRequest(policiesEnabledMap);
}
if (!this.enabled.value) {
if (policiesEnabledMap.get(PolicyType.RequireSso) ?? false) {
throw new Error(
this.i18nService.t("disableRequiredError", this.i18nService.t("requireSso")),
);
}
if (policiesEnabledMap.get(PolicyType.MaximumVaultTimeout) ?? false) {
throw new Error(
this.i18nService.t(
"disableRequiredError",
this.i18nService.t("maximumVaultTimeoutLabel"),
),
);
}
if (
(await firstValueFrom(this.accountDeprovisioningEnabled$)) &&
!this.policyResponse.canToggleState
) {
throw new Error(
this.i18nService.t("disableRequiredError", this.i18nService.t("singleOrg")),
);
}
}
return super.buildRequest(policiesEnabledMap);
}
}

View File

@@ -52,11 +52,7 @@
<form
*ngIf="org && !loading"
[bitSubmit]="submitCollectionManagement"
[formGroup]="
limitCollectionCreationDeletionSplitFeatureFlagIsEnabled
? collectionManagementFormGroup_VNext
: collectionManagementFormGroup
"
[formGroup]="collectionManagementFormGroup"
>
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "collectionManagement" | i18n }}</h1>
<p bitTypography="body1">{{ "collectionManagementDesc" | i18n }}</p>
@@ -64,24 +60,15 @@
<bit-label>{{ "allowAdminAccessToAllCollectionItemsDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="allowAdminAccessToAllCollectionItems" />
</bit-form-control>
<ng-container *ngIf="limitCollectionCreationDeletionSplitFeatureFlagIsEnabled">
<bit-form-control>
<bit-label>{{ "limitCollectionCreationDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="limitCollectionCreation" />
</bit-form-control>
<bit-form-control>
<bit-label>{{ "limitCollectionDeletionDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="limitCollectionDeletion" />
</bit-form-control>
</ng-container>
<ng-container *ngIf="!limitCollectionCreationDeletionSplitFeatureFlagIsEnabled">
<bit-form-control>
<bit-label>{{ "limitCollectionCreationDeletionDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="limitCollectionCreationDeletion" />
</bit-form-control>
</ng-container>
<bit-form-control>
<bit-label>{{ "limitCollectionCreationDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="limitCollectionCreation" />
</bit-form-control>
<bit-form-control>
<bit-label>{{ "limitCollectionDeletionDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="limitCollectionDeletion" />
</bit-form-control>
<button
*ngIf="!selfHosted || limitCollectionCreationDeletionSplitFeatureFlagIsEnabled"
type="submit"
bitButton
bitFormButton

View File

@@ -10,7 +10,6 @@ import { OrganizationCollectionManagementUpdateRequest } from "@bitwarden/common
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-update.request";
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -40,8 +39,6 @@ export class AccountComponent implements OnInit, OnDestroy {
org: OrganizationResponse;
taxFormPromise: Promise<unknown>;
limitCollectionCreationDeletionSplitFeatureFlagIsEnabled: boolean;
// FormGroup validators taken from server Organization domain object
protected formGroup = this.formBuilder.group({
orgName: this.formBuilder.control(
@@ -57,16 +54,7 @@ export class AccountComponent implements OnInit, OnDestroy {
),
});
// Deprecated. Delete with https://bitwarden.atlassian.net/browse/PM-10863
protected collectionManagementFormGroup = this.formBuilder.group({
limitCollectionCreationDeletion: this.formBuilder.control({ value: false, disabled: true }),
allowAdminAccessToAllCollectionItems: this.formBuilder.control({
value: false,
disabled: true,
}),
});
protected collectionManagementFormGroup_VNext = this.formBuilder.group({
limitCollectionCreation: this.formBuilder.control({ value: false, disabled: false }),
limitCollectionDeletion: this.formBuilder.control({ value: false, disabled: false }),
allowAdminAccessToAllCollectionItems: this.formBuilder.control({
@@ -98,11 +86,6 @@ export class AccountComponent implements OnInit, OnDestroy {
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.configService
.getFeatureFlag$(FeatureFlag.LimitCollectionCreationDeletionSplit)
.pipe(takeUntil(this.destroy$))
.subscribe((x) => (this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled = x));
this.route.params
.pipe(
switchMap((params) => this.organizationService.get$(params.organizationId)),
@@ -123,16 +106,6 @@ export class AccountComponent implements OnInit, OnDestroy {
this.canEditSubscription = organization.canEditSubscription;
this.canUseApi = organization.useApi;
// Disabling these fields for self hosted orgs is deprecated
// This block can be completely removed as part of
// https://bitwarden.atlassian.net/browse/PM-10863
if (!this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) {
if (!this.selfHosted) {
this.collectionManagementFormGroup.get("limitCollectionCreationDeletion").enable();
this.collectionManagementFormGroup.get("allowAdminAccessToAllCollectionItems").enable();
}
}
// Update disabled states - reactive forms prefers not using disabled attribute
if (!this.selfHosted) {
this.formGroup.get("orgName").enable();
@@ -152,18 +125,11 @@ export class AccountComponent implements OnInit, OnDestroy {
orgName: this.org.name,
billingEmail: this.org.billingEmail,
});
if (this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) {
this.collectionManagementFormGroup_VNext.patchValue({
limitCollectionCreation: this.org.limitCollectionCreation,
limitCollectionDeletion: this.org.limitCollectionDeletion,
allowAdminAccessToAllCollectionItems: this.org.allowAdminAccessToAllCollectionItems,
});
} else {
this.collectionManagementFormGroup.patchValue({
limitCollectionCreationDeletion: this.org.limitCollectionCreationDeletion,
allowAdminAccessToAllCollectionItems: this.org.allowAdminAccessToAllCollectionItems,
});
}
this.collectionManagementFormGroup.patchValue({
limitCollectionCreation: this.org.limitCollectionCreation,
limitCollectionDeletion: this.org.limitCollectionDeletion,
allowAdminAccessToAllCollectionItems: this.org.allowAdminAccessToAllCollectionItems,
});
this.loading = false;
});
@@ -211,24 +177,13 @@ export class AccountComponent implements OnInit, OnDestroy {
};
submitCollectionManagement = async () => {
// Early exit if self-hosted
if (this.selfHosted && !this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) {
return;
}
const request = new OrganizationCollectionManagementUpdateRequest();
if (this.limitCollectionCreationDeletionSplitFeatureFlagIsEnabled) {
request.limitCollectionCreation =
this.collectionManagementFormGroup_VNext.value.limitCollectionCreation;
request.limitCollectionDeletion =
this.collectionManagementFormGroup_VNext.value.limitCollectionDeletion;
request.allowAdminAccessToAllCollectionItems =
this.collectionManagementFormGroup_VNext.value.allowAdminAccessToAllCollectionItems;
} else {
request.limitCreateDeleteOwnerAdmin =
this.collectionManagementFormGroup.value.limitCollectionCreationDeletion;
request.allowAdminAccessToAllCollectionItems =
this.collectionManagementFormGroup.value.allowAdminAccessToAllCollectionItems;
}
request.limitCollectionCreation =
this.collectionManagementFormGroup.value.limitCollectionCreation;
request.limitCollectionDeletion =
this.collectionManagementFormGroup.value.limitCollectionDeletion;
request.allowAdminAccessToAllCollectionItems =
this.collectionManagementFormGroup.value.allowAdminAccessToAllCollectionItems;
await this.organizationApiService.updateCollectionManagement(this.organizationId, request);

View File

@@ -1,8 +1,11 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { inject, NgModule } from "@angular/core";
import { CanMatchFn, RouterModule, Routes } from "@angular/router";
import { map } from "rxjs";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
@@ -11,6 +14,11 @@ import { PoliciesComponent } from "../../organizations/policies";
import { AccountComponent } from "./account.component";
import { TwoFactorSetupComponent } from "./two-factor-setup.component";
const removeProviderExportPermission$: CanMatchFn = () =>
inject(ConfigService)
.getFeatureFlag$(FeatureFlag.PM11360RemoveProviderExportPermission)
.pipe(map((removeProviderExport) => removeProviderExport === true));
const routes: Routes = [
{
path: "",
@@ -53,18 +61,32 @@ const routes: Routes = [
path: "import",
loadComponent: () =>
import("./org-import.component").then((mod) => mod.OrgImportComponent),
canActivate: [organizationPermissionsGuard((org) => org.canAccessImportExport)],
canActivate: [organizationPermissionsGuard((org) => org.canAccessImport)],
data: {
titleId: "importData",
},
},
// Export routing is temporarily duplicated to set the flag value passed into org.canAccessExport
{
path: "export",
loadComponent: () =>
import("../tools/vault-export/org-vault-export.component").then(
(mod) => mod.OrganizationVaultExportComponent,
),
canMatch: [removeProviderExportPermission$], // if this matches, the flag is ON
canActivate: [organizationPermissionsGuard((org) => org.canAccessExport(true))],
data: {
titleId: "exportVault",
},
},
{
path: "export",
loadComponent: () =>
import("../tools/vault-export/org-vault-export.component").then(
(mod) => mod.OrganizationVaultExportComponent,
),
canActivate: [organizationPermissionsGuard((org) => org.canAccessImportExport)],
canActivate: [organizationPermissionsGuard((org) => org.canAccessExport(false))],
data: {
titleId: "exportVault",
},
@@ -82,7 +104,7 @@ function getSettingsRoute(organization: Organization) {
if (organization.canManagePolicies) {
return "policies";
}
if (organization.canAccessImportExport) {
if (organization.canAccessImport) {
return ["tools", "import"];
}
if (organization.canManageSso) {

View File

@@ -15,13 +15,13 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component";
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";
import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor/two-factor-verify.component";
@Component({
selector: "app-two-factor-setup",
templateUrl: "../../../auth/settings/two-factor-setup.component.html",
templateUrl: "../../../auth/settings/two-factor/two-factor-setup.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent implements OnInit {
@@ -79,12 +79,15 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
if (!result) {
return;
}
const duoComp: DialogRef<boolean, any> = TwoFactorDuoComponent.open(this.dialogService, {
data: {
authResponse: result,
organizationId: this.organizationId,
const duoComp: DialogRef<boolean, any> = TwoFactorSetupDuoComponent.open(
this.dialogService,
{
data: {
authResponse: result,
organizationId: this.organizationId,
},
},
});
);
this.twoFactorSetupSubscription = duoComp.componentInstance.onChangeStatus
.pipe(first(), takeUntil(this.destroy$))
.subscribe((enabled: boolean) => {

View File

@@ -104,6 +104,9 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
title: this.i18nService.t("errorOccured"),
message: this.i18nService.t("offerNoLongerValid"),
});
await this.router.navigate(["/"]);
return;
} else {
this.badToken = !this.preValidateSponsorshipResponse.isTokenValid;
}

View File

@@ -1 +1,19 @@
<router-outlet></router-outlet>
<ng-template #loadingState>
<!-- This is the same html from index.html which presents the bitwarden logo and loading spinny properly
when the body has the layout_frontend class. Having this match the index allows for a duplicative yet seamless
loading state here for process reloading. -->
<div class="tw-p-8 tw-flex">
<img class="new-logo-themed" alt="Bitwarden" />
<div class="spinner-container tw-justify-center">
<i
class="bwi bwi-spinner bwi-spin bwi-3x tw-text-muted"
title="Loading"
aria-hidden="true"
></i>
</div>
</div>
</ng-template>
<ng-container *ngIf="!loading; else loadingState">
<router-outlet></router-outlet>
</ng-container>

View File

@@ -6,7 +6,6 @@ import * as jq from "jquery";
import { Subject, filter, firstValueFrom, map, takeUntil, timeout, catchError, of } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { LogoutReason } from "@bitwarden/auth/common";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
@@ -17,6 +16,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
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";
@@ -28,7 +28,7 @@ import { StateEventRunnerService } from "@bitwarden/common/platform/state";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { DialogService, ToastOptions, ToastService } from "@bitwarden/components";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
@@ -60,6 +60,8 @@ export class AppComponent implements OnDestroy, OnInit {
private isIdle = false;
private destroy$ = new Subject<void>();
loading = false;
constructor(
@Inject(DOCUMENT) private document: Document,
private broadcasterService: BroadcasterService,
@@ -91,6 +93,7 @@ export class AppComponent implements OnDestroy, OnInit {
private accountService: AccountService,
private logService: LogService,
private sdkService: SdkService,
private processReloadService: ProcessReloadServiceAbstraction,
) {
if (flagEnabled("sdk")) {
// Warn if the SDK for some reason can't be initialized
@@ -161,7 +164,8 @@ export class AppComponent implements OnDestroy, OnInit {
this.router.navigate(["/"]);
break;
case "logout":
await this.logOut(message.logoutReason, message.redirect);
// note: the message.logoutReason isn't consumed anymore because of the process reload clearing any toasts.
await this.logOut(message.redirect);
break;
case "lockVault":
await this.vaultTimeoutService.lock();
@@ -170,9 +174,8 @@ export class AppComponent implements OnDestroy, OnInit {
// 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.notificationsService.updateConnection(false);
// 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.router.navigate(["lock"]);
await this.processReloadService.startProcessReload(this.authService);
break;
case "lockedUrl":
break;
@@ -272,33 +275,16 @@ export class AppComponent implements OnDestroy, OnInit {
this.destroy$.complete();
}
private async displayLogoutReason(logoutReason: LogoutReason) {
let toastOptions: ToastOptions;
switch (logoutReason) {
case "invalidSecurityStamp":
case "sessionExpired": {
toastOptions = {
variant: "warning",
title: this.i18nService.t("loggedOut"),
message: this.i18nService.t("loginExpired"),
};
break;
}
default: {
toastOptions = {
variant: "info",
title: this.i18nService.t("loggedOut"),
message: this.i18nService.t("loggedOutDesc"),
};
break;
}
}
private async logOut(redirect = true) {
// Ensure the loading state is applied before proceeding to avoid a flash
// of the login screen before the process reload fires.
this.ngZone.run(() => {
this.loading = true;
document.body.classList.add("layout_frontend");
});
this.toastService.showToast(toastOptions);
}
private async logOut(logoutReason: LogoutReason, redirect = true) {
await this.displayLogoutReason(logoutReason);
// Note: we don't display a toast logout reason anymore as the process reload
// will prevent any toasts from being displayed long enough to be read
await this.eventUploadService.uploadEvents();
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
@@ -334,10 +320,14 @@ export class AppComponent implements OnDestroy, OnInit {
await logoutPromise;
if (redirect) {
// 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.router.navigate(["/"]);
await this.router.navigate(["/"]);
}
await this.processReloadService.startProcessReload(this.authService);
// Normally we would need to reset the loading state to false or remove the layout_frontend
// class from the body here, but the process reload completely reloads the app so
// it handles it.
}, userId);
}

View File

@@ -1,4 +1,5 @@
export * from "./login";
export * from "./login-decryption-options";
export * from "./webauthn-login";
export * from "./set-password-jit";
export * from "./registration";

View File

@@ -0,0 +1 @@
export * from "./web-login-decryption-options.service";

View File

@@ -0,0 +1,41 @@
import { MockProxy, mock } from "jest-mock-extended";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { RouterService } from "../../../../core/router.service";
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
import { WebLoginDecryptionOptionsService } from "./web-login-decryption-options.service";
describe("WebLoginDecryptionOptionsService", () => {
let service: WebLoginDecryptionOptionsService;
let messagingService: MockProxy<MessagingService>;
let routerService: MockProxy<RouterService>;
let acceptOrganizationInviteService: MockProxy<AcceptOrganizationInviteService>;
beforeEach(() => {
messagingService = mock<MessagingService>();
routerService = mock<RouterService>();
acceptOrganizationInviteService = mock<AcceptOrganizationInviteService>();
service = new WebLoginDecryptionOptionsService(
messagingService,
routerService,
acceptOrganizationInviteService,
);
});
it("should instantiate the service", () => {
expect(service).not.toBeFalsy();
});
describe("handleCreateUserSuccess()", () => {
it("should clear the redirect URL and the org invite", async () => {
await service.handleCreateUserSuccess();
expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalled();
expect(acceptOrganizationInviteService.clearOrganizationInvitation).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,33 @@
import {
LoginDecryptionOptionsService,
DefaultLoginDecryptionOptionsService,
} from "@bitwarden/auth/angular";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { RouterService } from "../../../../core/router.service";
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
export class WebLoginDecryptionOptionsService
extends DefaultLoginDecryptionOptionsService
implements LoginDecryptionOptionsService
{
constructor(
protected messagingService: MessagingService,
private routerService: RouterService,
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
) {
super(messagingService);
}
override async handleCreateUserSuccess(): Promise<void> {
try {
// Invites from TDE orgs go through here, but the invite is
// accepted while being enrolled in admin recovery. So we need to clear
// the redirect and stored org invite.
await this.routerService.getAndClearLoginRedirectUrl();
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
} catch (error) {
throw new Error(error);
}
}
}

View File

@@ -7,13 +7,12 @@ 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 { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
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";
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
import { OrganizationInvite } from "../../../organization-invite/organization-invite";

View File

@@ -1,5 +1,5 @@
import { KdfType } from "@bitwarden/common/platform/enums";
import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
import { KdfType } from "@bitwarden/key-management";
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
import { EmergencyAccessType } from "../enums/emergency-access-type";

View File

@@ -1,6 +1,6 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { KdfType } from "@bitwarden/common/platform/enums";
import { CipherResponse } from "@bitwarden/common/vault/models/response/cipher.response";
import { KdfType } from "@bitwarden/key-management";
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
import { EmergencyAccessType } from "../enums/emergency-access-type";

View File

@@ -8,14 +8,14 @@ import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncryptionType, KdfType } from "@bitwarden/common/platform/enums";
import { EncryptionType } from "@bitwarden/common/platform/enums";
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";
import { UserId } from "@bitwarden/common/types/guid";
import { UserKey, MasterKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { KeyService } from "@bitwarden/key-management";
import { KdfType, KeyService } from "@bitwarden/key-management";
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
import { EmergencyAccessType } from "../enums/emergency-access-type";

View File

@@ -3,17 +3,11 @@ import { Injectable } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import {
Argon2KdfConfig,
KdfConfig,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { KdfType } from "@bitwarden/common/platform/enums";
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";
@@ -22,7 +16,14 @@ import { UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { UserKeyRotationDataProvider, KeyService } from "@bitwarden/key-management";
import {
Argon2KdfConfig,
KdfConfig,
PBKDF2KdfConfig,
UserKeyRotationDataProvider,
KeyService,
KdfType,
} from "@bitwarden/key-management";
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
import { EmergencyAccessType } from "../enums/emergency-access-type";

View File

@@ -1,14 +1,14 @@
import { Component, inject } from "@angular/core";
import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component";
import { BaseLoginDecryptionOptionsComponentV1 } from "@bitwarden/angular/auth/components/base-login-decryption-options-v1.component";
import { RouterService } from "../../../core";
import { AcceptOrganizationInviteService } from "../../organization-invite/accept-organization.service";
@Component({
selector: "web-login-decryption-options",
templateUrl: "login-decryption-options.component.html",
templateUrl: "login-decryption-options-v1.component.html",
})
export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent {
export class LoginDecryptionOptionsComponentV1 extends BaseLoginDecryptionOptionsComponentV1 {
protected routerService = inject(RouterService);
protected acceptOrganizationInviteService = inject(AcceptOrganizationInviteService);

View File

@@ -4,7 +4,7 @@ import { CheckboxModule } from "@bitwarden/components";
import { SharedModule } from "../../../app/shared";
import { LoginDecryptionOptionsComponent } from "./login-decryption-options/login-decryption-options.component";
import { LoginDecryptionOptionsComponentV1 } from "./login-decryption-options/login-decryption-options-v1.component";
import { LoginComponentV1 } from "./login-v1.component";
import { LoginViaAuthRequestComponentV1 } from "./login-via-auth-request-v1.component";
import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webauthn.component";
@@ -14,13 +14,13 @@ import { LoginViaWebAuthnComponent } from "./login-via-webauthn/login-via-webaut
declarations: [
LoginComponentV1,
LoginViaAuthRequestComponentV1,
LoginDecryptionOptionsComponent,
LoginDecryptionOptionsComponentV1,
LoginViaWebAuthnComponent,
],
exports: [
LoginComponentV1,
LoginViaAuthRequestComponentV1,
LoginDecryptionOptionsComponent,
LoginDecryptionOptionsComponentV1,
LoginViaWebAuthnComponent,
],
})

View File

@@ -2,7 +2,6 @@ import { Component, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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";
@@ -12,7 +11,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
@Component({
selector: "app-change-email",

View File

@@ -37,7 +37,7 @@
</button>
</div>
<div *ngIf="managingOrganization$ | async as managingOrganization">
{{ "accountIsManagedMessage" | i18n: managingOrganization?.name }}
{{ "accountIsOwnedMessage" | i18n: managingOrganization?.name }}
<a href="https://bitwarden.com/help/claimed-accounts">
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>

View File

@@ -7,7 +7,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
@@ -23,7 +22,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService } from "@bitwarden/key-management";
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
import { UserKeyRotationService } from "../../key-management/key-rotation/user-key-rotation.service";

View File

@@ -6,17 +6,15 @@ import { 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 { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/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 { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { KdfType } from "@bitwarden/common/platform/enums";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService } from "@bitwarden/key-management";
import { KdfType, KdfConfigService, KeyService } from "@bitwarden/key-management";
import { EmergencyAccessService } from "../../../emergency-access";

View File

@@ -5,14 +5,12 @@ import { firstValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
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 { KdfType } from "@bitwarden/common/platform/enums";
import { ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { KdfConfig, KdfType, KeyService } from "@bitwarden/key-management";
@Component({
selector: "app-change-kdf-confirmation",

View File

@@ -2,15 +2,15 @@ import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl, ValidatorFn, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { DialogService } from "@bitwarden/components";
import {
KdfConfigService,
Argon2KdfConfig,
DEFAULT_KDF_CONFIG,
KdfConfig,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import { KdfType } from "@bitwarden/common/platform/enums";
import { DialogService } from "@bitwarden/components";
KdfType,
} from "@bitwarden/key-management";
import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component";

View File

@@ -2,7 +2,7 @@ import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { ChangePasswordComponent } from "../change-password.component";
import { TwoFactorSetupComponent } from "../two-factor-setup.component";
import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";
import { SecurityKeysComponent } from "./security-keys.component";
import { SecurityComponent } from "./security.component";

View File

@@ -18,7 +18,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService, ToastService } from "@bitwarden/components";
import { TwoFactorBaseComponent } from "./two-factor-base.component";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
// NOTE: There are additional options available but these are just the ones we are current using.
// See: https://github.com/neocotic/qrious#examples
@@ -35,11 +35,11 @@ declare global {
}
@Component({
selector: "app-two-factor-authenticator",
templateUrl: "two-factor-authenticator.component.html",
selector: "app-two-factor-setup-authenticator",
templateUrl: "two-factor-setup-authenticator.component.html",
})
export class TwoFactorAuthenticatorComponent
extends TwoFactorBaseComponent
export class TwoFactorSetupAuthenticatorComponent
extends TwoFactorSetupMethodBaseComponent
implements OnInit, OnDestroy
{
@Output() onChangeStatus = new EventEmitter<boolean>();
@@ -200,7 +200,7 @@ export class TwoFactorAuthenticatorComponent
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorAuthenticatorResponse>>,
) {
return dialogService.open<boolean>(TwoFactorAuthenticatorComponent, config);
return dialogService.open<boolean>(TwoFactorSetupAuthenticatorComponent, config);
}
async launchExternalUrl(url: string) {

View File

@@ -13,13 +13,16 @@ 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 { TwoFactorBaseComponent } from "./two-factor-base.component";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
@Component({
selector: "app-two-factor-duo",
templateUrl: "two-factor-duo.component.html",
selector: "app-two-factor-setup-duo",
templateUrl: "two-factor-setup-duo.component.html",
})
export class TwoFactorDuoComponent extends TwoFactorBaseComponent implements OnInit {
export class TwoFactorSetupDuoComponent
extends TwoFactorSetupMethodBaseComponent
implements OnInit
{
@Output() onChangeStatus: EventEmitter<boolean> = new EventEmitter();
type = TwoFactorProviderType.Duo;
@@ -137,7 +140,7 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent implements OnI
dialogService: DialogService,
config: DialogConfig<TwoFactorDuoComponentConfig>,
) => {
return dialogService.open<boolean>(TwoFactorDuoComponent, config);
return dialogService.open<boolean>(TwoFactorSetupDuoComponent, config);
};
}

View File

@@ -16,14 +16,17 @@ 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 { TwoFactorBaseComponent } from "./two-factor-base.component";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
@Component({
selector: "app-two-factor-email",
templateUrl: "two-factor-email.component.html",
selector: "app-two-factor-setup-email",
templateUrl: "two-factor-setup-email.component.html",
outputs: ["onUpdated"],
})
export class TwoFactorEmailComponent extends TwoFactorBaseComponent implements OnInit {
export class TwoFactorSetupEmailComponent
extends TwoFactorSetupMethodBaseComponent
implements OnInit
{
@Output() onChangeStatus: EventEmitter<boolean> = new EventEmitter();
type = TwoFactorProviderType.Email;
sentEmail: string;
@@ -139,6 +142,6 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent implements O
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorEmailResponse>>,
) {
return dialogService.open<boolean>(TwoFactorEmailComponent, config);
return dialogService.open<boolean>(TwoFactorSetupEmailComponent, config);
}
}

View File

@@ -12,8 +12,11 @@ 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";
/**
* Base class for two-factor setup components (ex: email, yubikey, webauthn, duo).
*/
@Directive()
export abstract class TwoFactorBaseComponent {
export abstract class TwoFactorSetupMethodBaseComponent {
@Output() onUpdated = new EventEmitter<boolean>();
type: TwoFactorProviderType;

View File

@@ -18,7 +18,7 @@ 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 { TwoFactorBaseComponent } from "./two-factor-base.component";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
interface Key {
id: number;
@@ -29,10 +29,10 @@ interface Key {
}
@Component({
selector: "app-two-factor-webauthn",
templateUrl: "two-factor-webauthn.component.html",
selector: "app-two-factor-setup-webauthn",
templateUrl: "two-factor-setup-webauthn.component.html",
})
export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseComponent {
type = TwoFactorProviderType.WebAuthn;
name: string;
keys: Key[];
@@ -213,6 +213,6 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorWebAuthnResponse>>,
) {
return dialogService.open<boolean>(TwoFactorWebAuthnComponent, config);
return dialogService.open<boolean>(TwoFactorSetupWebAuthnComponent, config);
}
}

View File

@@ -13,7 +13,7 @@ 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 { TwoFactorBaseComponent } from "./two-factor-base.component";
import { TwoFactorSetupMethodBaseComponent } from "./two-factor-setup-method-base.component";
interface Key {
key: string;
@@ -21,10 +21,13 @@ interface Key {
}
@Component({
selector: "app-two-factor-yubikey",
templateUrl: "two-factor-yubikey.component.html",
selector: "app-two-factor-setup-yubikey",
templateUrl: "two-factor-setup-yubikey.component.html",
})
export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent implements OnInit {
export class TwoFactorSetupYubiKeyComponent
extends TwoFactorSetupMethodBaseComponent
implements OnInit
{
type = TwoFactorProviderType.Yubikey;
keys: Key[];
anyKeyHasNfc = false;
@@ -169,6 +172,6 @@ export class TwoFactorYubiKeyComponent extends TwoFactorBaseComponent implements
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorYubiKeyResponse>>,
) {
return dialogService.open<boolean>(TwoFactorYubiKeyComponent, config);
return dialogService.open<boolean>(TwoFactorSetupYubiKeyComponent, config);
}
}

View File

@@ -29,13 +29,13 @@ import { ProductTierType } from "@bitwarden/common/billing/enums";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";
import { TwoFactorAuthenticatorComponent } from "./two-factor-authenticator.component";
import { TwoFactorDuoComponent } from "./two-factor-duo.component";
import { TwoFactorEmailComponent } from "./two-factor-email.component";
import { TwoFactorRecoveryComponent } from "./two-factor-recovery.component";
import { TwoFactorSetupAuthenticatorComponent } from "./two-factor-setup-authenticator.component";
import { TwoFactorSetupDuoComponent } from "./two-factor-setup-duo.component";
import { TwoFactorSetupEmailComponent } from "./two-factor-setup-email.component";
import { TwoFactorSetupWebAuthnComponent } from "./two-factor-setup-webauthn.component";
import { TwoFactorSetupYubiKeyComponent } from "./two-factor-setup-yubikey.component";
import { TwoFactorVerifyComponent } from "./two-factor-verify.component";
import { TwoFactorWebAuthnComponent } from "./two-factor-webauthn.component";
import { TwoFactorYubiKeyComponent } from "./two-factor-yubikey.component";
@Component({
selector: "app-two-factor-setup",
@@ -142,7 +142,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) {
return;
}
const authComp: DialogRef<boolean, any> = TwoFactorAuthenticatorComponent.open(
const authComp: DialogRef<boolean, any> = TwoFactorSetupAuthenticatorComponent.open(
this.dialogService,
{ data: result },
);
@@ -160,7 +160,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) {
return;
}
const yubiComp: DialogRef<boolean, any> = TwoFactorYubiKeyComponent.open(
const yubiComp: DialogRef<boolean, any> = TwoFactorSetupYubiKeyComponent.open(
this.dialogService,
{ data: result },
);
@@ -177,11 +177,14 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) {
return;
}
const duoComp: DialogRef<boolean, any> = TwoFactorDuoComponent.open(this.dialogService, {
data: {
authResponse: result,
const duoComp: DialogRef<boolean, any> = TwoFactorSetupDuoComponent.open(
this.dialogService,
{
data: {
authResponse: result,
},
},
});
);
this.twoFactorSetupSubscription = duoComp.componentInstance.onChangeStatus
.pipe(first(), takeUntil(this.destroy$))
.subscribe((enabled: boolean) => {
@@ -196,7 +199,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) {
return;
}
const emailComp: DialogRef<boolean, any> = TwoFactorEmailComponent.open(
const emailComp: DialogRef<boolean, any> = TwoFactorSetupEmailComponent.open(
this.dialogService,
{
data: result,
@@ -216,7 +219,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) {
return;
}
const webAuthnComp: DialogRef<boolean, any> = TwoFactorWebAuthnComponent.open(
const webAuthnComp: DialogRef<boolean, any> = TwoFactorSetupWebAuthnComponent.open(
this.dialogService,
{ data: result },
);

View File

@@ -0,0 +1,125 @@
import { Injectable } from "@angular/core";
import { combineLatest, filter, from, map, Observable, of, switchMap } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
interface EnterpriseOrgStatus {
isFreeFamilyPolicyEnabled: boolean;
belongToOneEnterpriseOrgs: boolean;
belongToMultipleEnterpriseOrgs: boolean;
}
@Injectable({ providedIn: "root" })
export class FreeFamiliesPolicyService {
protected enterpriseOrgStatus: EnterpriseOrgStatus = {
isFreeFamilyPolicyEnabled: false,
belongToOneEnterpriseOrgs: false,
belongToMultipleEnterpriseOrgs: false,
};
constructor(
private policyService: PolicyService,
private organizationService: OrganizationService,
private configService: ConfigService,
) {}
get showFreeFamilies$(): Observable<boolean> {
return this.isFreeFamilyFlagEnabled$.pipe(
switchMap((isFreeFamilyFlagEnabled) =>
isFreeFamilyFlagEnabled
? this.getFreeFamiliesVisibility$()
: this.organizationService.canManageSponsorships$,
),
);
}
private getFreeFamiliesVisibility$(): Observable<boolean> {
return combineLatest([
this.checkEnterpriseOrganizationsAndFetchPolicy(),
this.organizationService.canManageSponsorships$,
]).pipe(
map(([orgStatus, canManageSponsorships]) =>
this.shouldShowFreeFamilyLink(orgStatus, canManageSponsorships),
),
);
}
private shouldShowFreeFamilyLink(
orgStatus: EnterpriseOrgStatus | null,
canManageSponsorships: boolean,
): boolean {
if (!orgStatus) {
return false;
}
const { belongToOneEnterpriseOrgs, isFreeFamilyPolicyEnabled } = orgStatus;
return canManageSponsorships && !(belongToOneEnterpriseOrgs && isFreeFamilyPolicyEnabled);
}
checkEnterpriseOrganizationsAndFetchPolicy(): Observable<EnterpriseOrgStatus> {
return this.organizationService.organizations$.pipe(
filter((organizations) => Array.isArray(organizations) && organizations.length > 0),
switchMap((organizations) => this.fetchEnterpriseOrganizationPolicy(organizations)),
);
}
private fetchEnterpriseOrganizationPolicy(
organizations: Organization[],
): Observable<EnterpriseOrgStatus> {
const { belongToOneEnterpriseOrgs, belongToMultipleEnterpriseOrgs } =
this.evaluateEnterpriseOrganizations(organizations);
if (!belongToOneEnterpriseOrgs) {
return of({
isFreeFamilyPolicyEnabled: false,
belongToOneEnterpriseOrgs,
belongToMultipleEnterpriseOrgs,
});
}
const organizationId = this.getOrganizationIdForOneEnterprise(organizations);
if (!organizationId) {
return of({
isFreeFamilyPolicyEnabled: false,
belongToOneEnterpriseOrgs,
belongToMultipleEnterpriseOrgs,
});
}
return this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy).pipe(
map((policies) => ({
isFreeFamilyPolicyEnabled: policies.some(
(policy) => policy.organizationId === organizationId && policy.enabled,
),
belongToOneEnterpriseOrgs,
belongToMultipleEnterpriseOrgs,
})),
);
}
private evaluateEnterpriseOrganizations(organizations: any[]): {
belongToOneEnterpriseOrgs: boolean;
belongToMultipleEnterpriseOrgs: boolean;
} {
const enterpriseOrganizations = organizations.filter((org) => org.canManageSponsorships);
const count = enterpriseOrganizations.length;
return {
belongToOneEnterpriseOrgs: count === 1,
belongToMultipleEnterpriseOrgs: count > 1,
};
}
private getOrganizationIdForOneEnterprise(organizations: any[]): string | null {
const enterpriseOrganizations = organizations.filter((org) => org.canManageSponsorships);
return enterpriseOrganizations.length === 1 ? enterpriseOrganizations[0].id : null;
}
private get isFreeFamilyFlagEnabled$(): Observable<boolean> {
return from(this.configService.getFeatureFlag(FeatureFlag.DisableFreeFamiliesSponsorship));
}
}

View File

@@ -8,13 +8,17 @@ import {
AsyncValidatorFn,
ValidationErrors,
} from "@angular/forms";
import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { combineLatest, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { PlanSponsorshipType } 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@@ -31,6 +35,7 @@ interface RequestSponsorshipForm {
})
export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
loading = false;
isFreeFamilyFlagEnabled: boolean;
availableSponsorshipOrgs$: Observable<Organization[]>;
activeSponsorshipOrgs$: Observable<Organization[]>;
@@ -53,6 +58,8 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
private formBuilder: FormBuilder,
private accountService: AccountService,
private toastService: ToastService,
private configService: ConfigService,
private policyService: PolicyService,
) {
this.sponsorshipForm = this.formBuilder.group<RequestSponsorshipForm>({
selectedSponsorshipOrgId: new FormControl("", {
@@ -72,10 +79,34 @@ export class SponsoredFamiliesComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe(
map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable)),
this.isFreeFamilyFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.DisableFreeFamiliesSponsorship,
);
if (this.isFreeFamilyFlagEnabled) {
this.availableSponsorshipOrgs$ = combineLatest([
this.organizationService.organizations$,
this.policyService.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy),
]).pipe(
map(([organizations, policies]) =>
organizations
.filter((org) => org.familySponsorshipAvailable)
.map((org) => ({
organization: org,
isPolicyEnabled: policies.some(
(policy) => policy.organizationId === org.id && policy.enabled,
),
}))
.filter(({ isPolicyEnabled }) => !isPolicyEnabled)
.map(({ organization }) => organization),
),
);
} else {
this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe(
map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable)),
);
}
this.availableSponsorshipOrgs$.pipe(takeUntil(this._destroy)).subscribe((orgs) => {
if (orgs.length === 1) {
this.sponsorshipForm.patchValue({

View File

@@ -18,7 +18,11 @@
<button
type="button"
bitMenuItem
*ngIf="!isSelfHosted && !sponsoringOrg.familySponsorshipValidUntil"
*ngIf="
!isSelfHosted &&
!sponsoringOrg.familySponsorshipValidUntil &&
!(isFreeFamilyPolicyEnabled$ | async)
"
(click)="resendEmail()"
[attr.aria-label]="'resendEmailLabel' | i18n: sponsoringOrg.familySponsorshipFriendlyName"
>

View File

@@ -1,9 +1,13 @@
import { formatDate } from "@angular/common";
import { Component, EventEmitter, Input, Output, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { firstValueFrom, map, Observable } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
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";
@@ -21,7 +25,8 @@ export class SponsoringOrgRowComponent implements OnInit {
statusMessage = "loading";
statusClass: "tw-text-success" | "tw-text-danger" = "tw-text-success";
isFreeFamilyPolicyEnabled$: Observable<boolean>;
isFreeFamilyFlagEnabled: boolean;
private locale = "";
constructor(
@@ -31,6 +36,8 @@ export class SponsoringOrgRowComponent implements OnInit {
private platformUtilsService: PlatformUtilsService,
private dialogService: DialogService,
private toastService: ToastService,
private configService: ConfigService,
private policyService: PolicyService,
) {}
async ngOnInit() {
@@ -42,6 +49,23 @@ export class SponsoringOrgRowComponent implements OnInit {
this.sponsoringOrg.familySponsorshipValidUntil,
this.sponsoringOrg.familySponsorshipLastSyncDate,
);
this.isFreeFamilyFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.DisableFreeFamiliesSponsorship,
);
if (this.isFreeFamilyFlagEnabled) {
this.isFreeFamilyPolicyEnabled$ = this.policyService
.getAll$(PolicyType.FreeFamiliesSponsorshipPolicy)
.pipe(
map(
(policies) =>
Array.isArray(policies) &&
policies.some(
(policy) => policy.organizationId === this.sponsoringOrg.id && policy.enabled,
),
),
);
}
}
async revokeSponsorship() {

View File

@@ -0,0 +1,5 @@
<bit-nav-item
[text]="'sponsoredFamilies' | i18n"
route="settings/sponsored-families"
*ngIf="showFreeFamilies$ | async"
></bit-nav-item>

View File

@@ -0,0 +1,22 @@
import { Component } from "@angular/core";
import { Observable } from "rxjs";
import { NavigationModule } from "@bitwarden/components";
import { FreeFamiliesPolicyService } from "../services/free-families-policy.service";
import { BillingSharedModule } from "./billing-shared.module";
@Component({
selector: "billing-free-families-nav-item",
templateUrl: "./billing-free-families-nav-item.component.html",
standalone: true,
imports: [NavigationModule, BillingSharedModule],
})
export class BillingFreeFamiliesNavItemComponent {
showFreeFamilies$: Observable<boolean>;
constructor(private freeFamiliesPolicyService: FreeFamiliesPolicyService) {
this.showFreeFamilies$ = this.freeFamiliesPolicyService.showFreeFamilies$;
}
}

View File

@@ -80,9 +80,9 @@
</ng-container>
<!-- Bank Account -->
<ng-container *ngIf="showBankAccount && usingBankAccount">
<app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
{{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}
</app-callout>
<bit-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
{{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }}
</bit-callout>
<div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4" formGroupName="bankInformation">
<bit-form-field class="tw-col-span-1" disableMargin>
<bit-label>{{ "routingNumber" | i18n }}</bit-label>

View File

@@ -1,18 +1,12 @@
<app-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
<p>{{ "verifyBankAccountDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}</p>
<bit-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
<p>{{ "verifyBankAccountWithStatementDescriptorInstructions" | i18n }}</p>
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-form-field class="tw-mr-2 tw-w-40">
<bit-label>{{ "amountX" | i18n: "1" }}</bit-label>
<input bitInput type="number" step="1" placeholder="xx" formControlName="amount1" />
<span bitPrefix>$0.</span>
</bit-form-field>
<bit-form-field class="tw-mr-2 tw-w-40">
<bit-label>{{ "amountX" | i18n: "2" }}</bit-label>
<input bitInput type="number" step="1" placeholder="xx" formControlName="amount2" />
<span bitPrefix>$0.</span>
<bit-form-field class="tw-mr-2 tw-w-48">
<bit-label>{{ "descriptorCode" | i18n }}</bit-label>
<input bitInput type="text" placeholder="SMAB12" formControlName="descriptorCode" />
</bit-form-field>
<button *ngIf="onSubmit" type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button>
</form>
</app-callout>
</bit-callout>

View File

@@ -16,25 +16,17 @@ export class VerifyBankAccountComponent {
@Output() submitted = new EventEmitter();
protected formGroup = this.formBuilder.group({
amount1: new FormControl<number>(null, [
descriptorCode: new FormControl<string>(null, [
Validators.required,
Validators.min(0),
Validators.max(99),
]),
amount2: new FormControl<number>(null, [
Validators.required,
Validators.min(0),
Validators.max(99),
Validators.minLength(6),
Validators.maxLength(6),
]),
});
constructor(private formBuilder: FormBuilder) {}
submit = async () => {
const request = new VerifyBankAccountRequest(
this.formGroup.value.amount1,
this.formGroup.value.amount2,
);
const request = new VerifyBankAccountRequest(this.formGroup.value.descriptorCode);
await this.onSubmit?.(request);
this.submitted.emit();
};

View File

@@ -30,6 +30,7 @@ import {
LoginComponentService,
LockComponentService,
SetPasswordJitService,
LoginDecryptionOptionsService,
} from "@bitwarden/auth/angular";
import {
InternalUserDecryptionOptionsServiceAbstraction,
@@ -45,10 +46,10 @@ import {
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ClientType } from "@bitwarden/common/enums";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@@ -60,6 +61,7 @@ import {
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService as I18nServiceAbstraction } 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 { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
@@ -82,7 +84,11 @@ import {
} from "@bitwarden/common/platform/theming/theme-state.service";
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { KeyService as KeyServiceAbstraction, BiometricsService } from "@bitwarden/key-management";
import {
KdfConfigService,
KeyService as KeyServiceAbstraction,
BiometricsService,
} from "@bitwarden/key-management";
import { flagEnabled } from "../../utils/flags";
import { PolicyListService } from "../admin-console/core/policy-list.service";
@@ -91,10 +97,12 @@ import {
WebRegistrationFinishService,
WebLoginComponentService,
WebLockComponentService,
WebLoginDecryptionOptionsService,
} from "../auth";
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
import { HtmlStorageService } from "../core/html-storage.service";
import { I18nService } from "../core/i18n.service";
import { WebProcessReloadService } from "../key-management/services/web-process-reload.service";
import { WebBiometricsService } from "../key-management/web-biometric.service";
import { WebEnvironmentService } from "../platform/web-environment.service";
import { WebMigrationRunner } from "../platform/web-migration-runner";
@@ -281,11 +289,21 @@ const safeProviders: SafeProvider[] = [
useClass: flagEnabled("sdk") ? WebSdkClientFactory : NoopSdkClientFactory,
deps: [],
}),
safeProvider({
provide: ProcessReloadServiceAbstraction,
useClass: WebProcessReloadService,
deps: [WINDOW],
}),
safeProvider({
provide: LoginEmailService,
useClass: LoginEmailService,
deps: [AccountService, AuthService, StateProvider],
}),
safeProvider({
provide: LoginDecryptionOptionsService,
useClass: WebLoginDecryptionOptionsService,
deps: [MessagingService, RouterService, AcceptOrganizationInviteService],
}),
];
@NgModule({

View File

@@ -0,0 +1,14 @@
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
export class WebProcessReloadService implements ProcessReloadServiceAbstraction {
constructor(private window: Window) {}
async startProcessReload(authService: AuthService): Promise<void> {
this.window.location.reload();
}
cancelProcessReload(): void {
return;
}
}

View File

@@ -152,7 +152,13 @@ export const SMAvailable: Story = {
...Template,
args: {
mockOrgs: [
{ id: "org-a", canManageUsers: false, canAccessSecretsManager: true, enabled: true },
{
id: "org-a",
canManageUsers: false,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [],
},
@@ -162,7 +168,13 @@ export const SMAndACAvailable: Story = {
...Template,
args: {
mockOrgs: [
{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true },
{
id: "org-a",
canManageUsers: true,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [],
},
@@ -172,7 +184,13 @@ export const WithAllOptions: Story = {
...Template,
args: {
mockOrgs: [
{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true },
{
id: "org-a",
canManageUsers: true,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [{ id: "provider-a" }] as Provider[],
},

View File

@@ -171,7 +171,13 @@ export const WithSM: Story = {
...Template,
args: {
mockOrgs: [
{ id: "org-a", canManageUsers: false, canAccessSecretsManager: true, enabled: true },
{
id: "org-a",
canManageUsers: false,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [],
},
@@ -181,7 +187,13 @@ export const WithSMAndAC: Story = {
...Template,
args: {
mockOrgs: [
{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true },
{
id: "org-a",
canManageUsers: true,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [],
},
@@ -191,7 +203,13 @@ export const WithAllOptions: Story = {
...Template,
args: {
mockOrgs: [
{ id: "org-a", canManageUsers: true, canAccessSecretsManager: true, enabled: true },
{
id: "org-a",
canManageUsers: true,
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => false,
},
] as Organization[],
mockProviders: [{ id: "provider-a" }] as Provider[],
},

View File

@@ -110,7 +110,12 @@ describe("ProductSwitcherService", () => {
it("is included in bento when there is an organization with SM", async () => {
organizationService.organizations$ = of([
{ id: "1234", canAccessSecretsManager: true, enabled: true },
{
id: "1234",
canAccessSecretsManager: true,
enabled: true,
canAccessExport: (_) => true,
},
] as Organization[]);
initiateService();
@@ -220,8 +225,20 @@ describe("ProductSwitcherService", () => {
router.url = "/sm/4243";
organizationService.organizations$ = of([
{ id: "23443234", canAccessSecretsManager: true, enabled: true, name: "Org 2" },
{ id: "4243", canAccessSecretsManager: true, enabled: true, name: "Org 32" },
{
id: "23443234",
canAccessSecretsManager: true,
enabled: true,
name: "Org 2",
canAccessExport: (_) => true,
},
{
id: "4243",
canAccessSecretsManager: true,
enabled: true,
name: "Org 32",
canAccessExport: (_) => true,
},
] as Organization[]);
initiateService();

View File

@@ -24,11 +24,7 @@
[text]="'emergencyAccess' | i18n"
route="settings/emergency-access"
></bit-nav-item>
<bit-nav-item
[text]="'sponsoredFamilies' | i18n"
route="settings/sponsored-families"
*ngIf="hasFamilySponsorshipAvailable$ | async"
></bit-nav-item>
<billing-free-families-nav-item></billing-free-families-nav-item>
</bit-nav-group>
</app-side-nav>

View File

@@ -1,16 +1,17 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Observable, combineLatest, concatMap } from "rxjs";
import { Observable, concatMap, combineLatest } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { IconModule } from "@bitwarden/components";
import { BillingFreeFamiliesNavItemComponent } from "../billing/shared/billing-free-families-nav-item.component";
import { PasswordManagerLogo } from "./password-manager-logo";
import { WebLayoutModule } from "./web-layout.module";
@@ -18,16 +19,24 @@ import { WebLayoutModule } from "./web-layout.module";
selector: "app-user-layout",
templateUrl: "user-layout.component.html",
standalone: true,
imports: [CommonModule, RouterModule, JslibModule, WebLayoutModule, IconModule],
imports: [
CommonModule,
RouterModule,
JslibModule,
WebLayoutModule,
IconModule,
BillingFreeFamiliesNavItemComponent,
],
})
export class UserLayoutComponent implements OnInit {
protected readonly logo = PasswordManagerLogo;
isFreeFamilyFlagEnabled: boolean;
protected hasFamilySponsorshipAvailable$: Observable<boolean>;
protected showSponsoredFamilies$: Observable<boolean>;
protected showSubscription$: Observable<boolean>;
constructor(
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private apiService: ApiService,
private syncService: SyncService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
@@ -38,8 +47,6 @@ export class UserLayoutComponent implements OnInit {
await this.syncService.fullSync(false);
this.hasFamilySponsorshipAvailable$ = this.organizationService.canManageSponsorships$;
// We want to hide the subscription menu for organizations that provide premium.
// Except if the user has premium personally or has a billing history.
this.showSubscription$ = combineLatest([

View File

@@ -1,6 +1,7 @@
import { NgModule } from "@angular/core";
import { Route, RouterModule, Routes } from "@angular/router";
import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component";
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
import {
authGuard,
@@ -26,6 +27,7 @@ import {
LoginSecondaryContentComponent,
LockV2Component,
LockIcon,
TwoFactorTimeoutIcon,
UserLockIcon,
LoginViaAuthRequestComponent,
DevicesIcon,
@@ -33,6 +35,7 @@ import {
RegistrationLockAltIcon,
RegistrationExpiredLinkIcon,
VaultIcon,
LoginDecryptionOptionsComponent,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -46,7 +49,7 @@ import { CreateOrganizationComponent } from "./admin-console/settings/create-org
import { deepLinkGuard } from "./auth/guards/deep-link.guard";
import { HintComponent } from "./auth/hint.component";
import { LockComponent } from "./auth/lock.component";
import { LoginDecryptionOptionsComponent } from "./auth/login/login-decryption-options/login-decryption-options.component";
import { LoginDecryptionOptionsComponentV1 } from "./auth/login/login-decryption-options/login-decryption-options-v1.component";
import { LoginComponentV1 } from "./auth/login/login-v1.component";
import { LoginViaAuthRequestComponentV1 } from "./auth/login/login-via-auth-request-v1.component";
import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login-via-webauthn.component";
@@ -103,11 +106,6 @@ const routes: Routes = [
component: LoginViaWebAuthnComponent,
data: { titleId: "logInWithPasskey" } satisfies RouteDataProperties,
},
{
path: "login-initiated",
component: LoginDecryptionOptionsComponent,
canActivate: [tdeDecryptionRequiredGuard()],
},
{
path: "register",
component: TrialInitiationComponent,
@@ -272,6 +270,22 @@ const routes: Routes = [
],
},
),
...unauthUiRefreshSwap(
LoginDecryptionOptionsComponentV1,
AnonLayoutWrapperComponent,
{
path: "login-initiated",
canActivate: [tdeDecryptionRequiredGuard()],
},
{
path: "login-initiated",
canActivate: [tdeDecryptionRequiredGuard()],
data: {
pageIcon: DevicesIcon,
},
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
},
),
...unauthUiRefreshSwap(
AnonLayoutWrapperComponent,
AnonLayoutWrapperComponent,
@@ -495,7 +509,6 @@ const routes: Routes = [
} satisfies AnonLayoutWrapperData,
},
),
{
path: "2fa",
canActivate: [unauthGuardFn()],
@@ -515,6 +528,28 @@ const routes: Routes = [
},
} satisfies RouteDataProperties & AnonLayoutWrapperData,
},
{
path: "2fa-timeout",
canActivate: [unauthGuardFn()],
children: [
{
path: "",
component: TwoFactorTimeoutComponent,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
data: {
pageIcon: TwoFactorTimeoutIcon,
pageTitle: {
key: "authenticationTimeout",
},
titleId: "authenticationTimeout",
} satisfies RouteDataProperties & AnonLayoutWrapperData,
},
{
path: "recover-2fa",
canActivate: [unauthGuardFn()],

View File

@@ -41,14 +41,14 @@ import { ApiKeyComponent } from "../auth/settings/security/api-key.component";
import { ChangeKdfModule } from "../auth/settings/security/change-kdf/change-kdf.module";
import { SecurityKeysComponent } from "../auth/settings/security/security-keys.component";
import { SecurityComponent } from "../auth/settings/security/security.component";
import { TwoFactorAuthenticatorComponent } from "../auth/settings/two-factor-authenticator.component";
import { TwoFactorDuoComponent } from "../auth/settings/two-factor-duo.component";
import { TwoFactorEmailComponent } from "../auth/settings/two-factor-email.component";
import { TwoFactorRecoveryComponent } from "../auth/settings/two-factor-recovery.component";
import { TwoFactorSetupComponent } from "../auth/settings/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor-verify.component";
import { TwoFactorWebAuthnComponent } from "../auth/settings/two-factor-webauthn.component";
import { TwoFactorYubiKeyComponent } from "../auth/settings/two-factor-yubikey.component";
import { TwoFactorRecoveryComponent } from "../auth/settings/two-factor/two-factor-recovery.component";
import { TwoFactorSetupAuthenticatorComponent } from "../auth/settings/two-factor/two-factor-setup-authenticator.component";
import { TwoFactorSetupDuoComponent } from "../auth/settings/two-factor/two-factor-setup-duo.component";
import { TwoFactorSetupEmailComponent } from "../auth/settings/two-factor/two-factor-setup-email.component";
import { TwoFactorSetupWebAuthnComponent } from "../auth/settings/two-factor/two-factor-setup-webauthn.component";
import { TwoFactorSetupYubiKeyComponent } from "../auth/settings/two-factor/two-factor-setup-yubikey.component";
import { TwoFactorSetupComponent } from "../auth/settings/two-factor/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor-verify.component";
import { UserVerificationModule } from "../auth/shared/components/user-verification";
import { SsoComponent } from "../auth/sso.component";
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
@@ -159,16 +159,16 @@ import { SharedModule } from "./shared.module";
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
TwoFactorAuthenticatorComponent,
TwoFactorSetupAuthenticatorComponent,
TwoFactorComponent,
TwoFactorDuoComponent,
TwoFactorEmailComponent,
TwoFactorSetupDuoComponent,
TwoFactorSetupEmailComponent,
TwoFactorOptionsComponent,
TwoFactorRecoveryComponent,
TwoFactorSetupComponent,
TwoFactorVerifyComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
TwoFactorSetupWebAuthnComponent,
TwoFactorSetupYubiKeyComponent,
UpdatePasswordComponent,
UpdateTempPasswordComponent,
VerifyEmailTokenComponent,
@@ -226,16 +226,16 @@ import { SharedModule } from "./shared.module";
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponent,
TwoFactorAuthenticatorComponent,
TwoFactorSetupAuthenticatorComponent,
TwoFactorComponent,
TwoFactorDuoComponent,
TwoFactorEmailComponent,
TwoFactorSetupDuoComponent,
TwoFactorSetupEmailComponent,
TwoFactorOptionsComponent,
TwoFactorRecoveryComponent,
TwoFactorSetupComponent,
TwoFactorVerifyComponent,
TwoFactorWebAuthnComponent,
TwoFactorYubiKeyComponent,
TwoFactorSetupWebAuthnComponent,
TwoFactorSetupYubiKeyComponent,
UpdatePasswordComponent,
UpdateTempPasswordComponent,
UserLayoutComponent,

View File

@@ -1,24 +0,0 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { RiskInsightsComponent } from "./risk-insights.component";
const routes: Routes = [
{
path: "risk-insights",
canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)],
component: RiskInsightsComponent,
data: {
titleId: "RiskInsights",
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AccessIntelligenceRoutingModule {}

View File

@@ -1,9 +0,0 @@
import { NgModule } from "@angular/core";
import { AccessIntelligenceRoutingModule } from "./access-intelligence-routing.module";
import { RiskInsightsComponent } from "./risk-insights.component";
@NgModule({
imports: [RiskInsightsComponent, AccessIntelligenceRoutingModule],
})
export class AccessIntelligenceModule {}

View File

@@ -1,114 +0,0 @@
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
{{ "noAppsInOrgTitle" | i18n: organization.name }}
</h2>
</ng-container>
<ng-container slot="description">
<div class="tw-flex tw-flex-col tw-mb-2">
<span class="tw-text-muted">
{{ "noAppsInOrgDescription" | i18n }}
</span>
<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
</div>
</ng-container>
<ng-container slot="button">
<button (click)="goToCreateNewLoginItem()" bitButton buttonType="primary" type="button">
{{ "createNewLoginItem" | i18n }}
</button>
</ng-container>
</bit-no-items>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="mockAtRiskMembersCount"
[maxValue]="mockTotalMembersCount"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="mockAtRiskAppsCount"
[maxValue]="mockTotalAppsCount"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"
class="tw-grow"
[formControl]="searchControl"
></bit-search>
<button
class="tw-rounded-lg"
type="button"
buttonType="secondary"
bitButton
*ngIf="isCritialAppsFeatureEnabled"
[disabled]="!selectedIds.size"
[loading]="markingAsCritical"
(click)="markAppsAsCritical()"
>
<i class="bwi bwi-star-f tw-mr-2"></i>
{{ "markAppAsCritical" | i18n }}
</button>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th *ngIf="isCritialAppsFeatureEnabled"></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td *ngIf="isCritialAppsFeatureEnabled">
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<span>{{ r.name }}</span>
</td>
<td bitCell>
<span>
{{ r.atRiskPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.totalPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.atRiskMembers }}
</span>
</td>
<td bitCell data-testid="total-membership">
{{ r.totalMembers }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@@ -1,126 +0,0 @@
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { debounceTime, firstValueFrom, map } from "rxjs";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
Icons,
NoItemsModule,
SearchModule,
TableDataSource,
ToastService,
} from "@bitwarden/components";
import { CardComponent } from "@bitwarden/tools-card";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { applicationTableMockData } from "./application-table.mock";
@Component({
standalone: true,
selector: "tools-all-applications",
templateUrl: "./all-applications.component.html",
imports: [HeaderModule, CardComponent, SearchModule, PipesModule, NoItemsModule, SharedModule],
})
export class AllApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<any>();
protected selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
protected loading = false;
protected organization: Organization;
noItemsIcon = Icons.Security;
protected markingAsCritical = false;
isCritialAppsFeatureEnabled = false;
// MOCK DATA
protected mockData = applicationTableMockData;
protected mockAtRiskMembersCount = 0;
protected mockAtRiskAppsCount = 0;
protected mockTotalMembersCount = 0;
protected mockTotalAppsCount = 0;
async ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
const organizationId = params.get("organizationId");
this.organization = await firstValueFrom(this.organizationService.get$(organizationId));
// TODO: use organizationId to fetch data
}),
)
.subscribe();
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
}
constructor(
protected cipherService: CipherService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected auditService: AuditService,
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected toastService: ToastService,
protected organizationService: OrganizationService,
protected configService: ConfigService,
) {
this.dataSource.data = applicationTableMockData;
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
goToCreateNewLoginItem = async () => {
// TODO: implement
this.toastService.showToast({
variant: "warning",
title: null,
message: "Not yet implemented",
});
};
markAppsAsCritical = async () => {
// TODO: Send to API once implemented
this.markingAsCritical = true;
return new Promise((resolve) => {
setTimeout(() => {
this.selectedIds.clear();
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("appsMarkedAsCritical"),
});
resolve(true);
this.markingAsCritical = false;
}, 1000);
});
};
trackByFunction(_: number, item: CipherView) {
return item.id;
}
onCheckboxChange(id: number, event: Event) {
const isChecked = (event.target as HTMLInputElement).checked;
if (isChecked) {
this.selectedIds.add(id);
} else {
this.selectedIds.delete(id);
}
}
}

View File

@@ -1,50 +0,0 @@
export const applicationTableMockData = [
{
id: 1,
name: "google.com",
atRiskPasswords: 4,
totalPasswords: 10,
atRiskMembers: 2,
totalMembers: 5,
},
{
id: 2,
name: "facebook.com",
atRiskPasswords: 3,
totalPasswords: 8,
atRiskMembers: 1,
totalMembers: 3,
},
{
id: 3,
name: "twitter.com",
atRiskPasswords: 2,
totalPasswords: 6,
atRiskMembers: 0,
totalMembers: 2,
},
{
id: 4,
name: "linkedin.com",
atRiskPasswords: 1,
totalPasswords: 4,
atRiskMembers: 0,
totalMembers: 1,
},
{
id: 5,
name: "instagram.com",
atRiskPasswords: 0,
totalPasswords: 2,
atRiskMembers: 0,
totalMembers: 0,
},
{
id: 6,
name: "tiktok.com",
atRiskPasswords: 0,
totalPasswords: 1,
atRiskMembers: 0,
totalMembers: 0,
},
];

View File

@@ -1,115 +0,0 @@
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!dataSource.data.length">
<bit-no-items [icon]="noItemsIcon" class="tw-text-main">
<ng-container slot="title">
<h2 class="tw-font-semibold mt-4">
{{ "noCriticalAppsTitle" | i18n }}
</h2>
</ng-container>
<ng-container slot="description">
<p class="tw-text-muted">
{{ "noCriticalAppsDescription" | i18n }}
</p>
</ng-container>
<ng-container slot="button">
<button (click)="goToAllAppsTab()" bitButton buttonType="primary" type="button">
{{ "markCriticalApps" | i18n }}
</button>
</ng-container>
</bit-no-items>
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<div class="tw-flex tw-justify-between tw-mb-4">
<h2 bitTypography="h2">{{ "criticalApplications" | i18n }}</h2>
<button bitButton buttonType="primary" type="button">
<i class="bwi bwi-envelope tw-mr-2"></i>
{{ "requestPasswordChange" | i18n }}
</button>
</div>
<div class="tw-flex tw-gap-6">
<tools-card
class="tw-flex-1"
[title]="'atRiskMembers' | i18n"
[value]="mockAtRiskMembersCount"
[maxValue]="mockTotalMembersCount"
>
</tools-card>
<tools-card
class="tw-flex-1"
[title]="'atRiskApplications' | i18n"
[value]="mockAtRiskAppsCount"
[maxValue]="mockTotalAppsCount"
>
</tools-card>
</div>
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"
class="tw-grow"
[formControl]="searchControl"
></bit-search>
</div>
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
<th></th>
<th bitSortable="name" bitCell>{{ "application" | i18n }}</th>
<th bitSortable="atRiskPasswords" bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitSortable="totalPasswords" bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitSortable="atRiskMembers" bitCell>{{ "atRiskMembers" | i18n }}</th>
<th bitSortable="totalMembers" bitCell>{{ "totalMembers" | i18n }}</th>
<th></th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td>
<i class="bwi bwi-star-f"></i>
</td>
<td bitCell>
<span>{{ r.name }}</span>
</td>
<td bitCell>
<span>
{{ r.atRiskPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.totalPasswords }}
</span>
</td>
<td bitCell>
<span>
{{ r.atRiskMembers }}
</span>
</td>
<td bitCell data-testid="total-membership">
{{ r.totalMembers }}
</td>
<td bitCell>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #rowMenu>
<button type="button" bitMenuItem (click)="unmarkAsCriticalApp(r.name)">
<i aria-hidden="true" class="bwi bwi-star-f"></i> {{ "unmarkAsCriticalApp" | i18n }}
</button>
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@@ -1,96 +0,0 @@
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { debounceTime, map } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import { UnmarkCriticalApplicationApiService } from "@bitwarden/bit-common/tools/reports/risk-insights";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
SearchModule,
TableDataSource,
NoItemsModule,
Icons,
ToastService,
} from "@bitwarden/components";
import { CardComponent } from "@bitwarden/tools-card";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { applicationTableMockData } from "./application-table.mock";
import { RiskInsightsTabType } from "./risk-insights.component";
@Component({
standalone: true,
selector: "tools-critical-applications",
templateUrl: "./critical-applications.component.html",
imports: [CardComponent, HeaderModule, SearchModule, NoItemsModule, PipesModule, SharedModule],
providers: [UnmarkCriticalApplicationApiService],
})
export class CriticalApplicationsComponent implements OnInit {
protected dataSource = new TableDataSource<any>();
protected selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
protected loading = false;
protected organizationId: string;
noItemsIcon = Icons.Security;
// MOCK DATA
protected mockData = applicationTableMockData;
protected mockAtRiskMembersCount = 0;
protected mockAtRiskAppsCount = 0;
protected mockTotalMembersCount = 0;
protected mockTotalAppsCount = 0;
ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
this.organizationId = params.get("organizationId");
// TODO: use organizationId to fetch data
}),
)
.subscribe();
}
goToAllAppsTab = async () => {
await this.router.navigate([`organizations/${this.organizationId}/risk-insights`], {
queryParams: { tabIndex: RiskInsightsTabType.AllApps },
queryParamsHandling: "merge",
});
};
unmarkAsCriticalApp = async (hostname: string) => {
await this.unmarkCriticalApplicationApiService.unmarkCriticalApplication(
this.organizationId,
hostname,
);
this.toastService.showToast({
// TODO uncomment when UnmarkCriticalApplicationApiService is properly implemented
// message: this.i18nService.t("criticalApplicationSuccessfullyUnmarked"),
// variant: "success",
// title: this.i18nService.t("Success"),
title: "API not yet implemented",
variant: "warning",
message: "API not yet implemented",
});
this.dataSource.data = this.dataSource.data.filter((app) => app.name !== hostname);
};
constructor(
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected router: Router,
private unmarkCriticalApplicationApiService: UnmarkCriticalApplicationApiService,
protected toastService: ToastService,
) {
this.dataSource.data = applicationTableMockData;
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
}

View File

@@ -1,11 +0,0 @@
<!-- <bit-table [dataSource]="dataSource"> -->
<ng-container header>
<tr>
<th bitCell>{{ "member" | i18n }}</th>
<th bitCell>{{ "atRiskPasswords" | i18n }}</th>
<th bitCell>{{ "totalPasswords" | i18n }}</th>
<th bitCell>{{ "atRiskApplications" | i18n }}</th>
<th bitCell>{{ "totalApplications" | i18n }}</th>
</tr>
</ng-container>
<!-- </bit-table> -->

View File

@@ -1,19 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { TableDataSource, TableModule } from "@bitwarden/components";
@Component({
standalone: true,
selector: "tools-notified-members-table",
templateUrl: "./notified-members-table.component.html",
imports: [CommonModule, JslibModule, TableModule],
})
export class NotifiedMembersTableComponent {
dataSource = new TableDataSource<any>();
constructor() {
this.dataSource.data = [];
}
}

View File

@@ -1,55 +0,0 @@
<bit-container>
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!loading">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell bitSortable="hostURI">{{ "application" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>
<ng-container>
<span>{{ r.hostURI }}</span>
</ng-container>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>
</bit-container>

View File

@@ -1,73 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, convertToParamMap } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { of } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import {
MemberCipherDetailsApiService,
PasswordHealthService,
} from "@bitwarden/bit-common/tools/reports/risk-insights";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TableModule } from "@bitwarden/components";
import { LooseComponentsModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { PasswordHealthMembersURIComponent } from "./password-health-members-uri.component";
describe("PasswordHealthMembersUriComponent", () => {
let component: PasswordHealthMembersURIComponent;
let fixture: ComponentFixture<PasswordHealthMembersURIComponent>;
let cipherServiceMock: MockProxy<CipherService>;
const passwordHealthServiceMock = mock<PasswordHealthService>();
const activeRouteParams = convertToParamMap({ organizationId: "orgId" });
beforeEach(async () => {
cipherServiceMock = mock<CipherService>();
await TestBed.configureTestingModule({
imports: [PasswordHealthMembersURIComponent, PipesModule, TableModule, LooseComponentsModule],
providers: [
{ provide: CipherService, useValue: cipherServiceMock },
{ provide: I18nService, useValue: mock<I18nService>() },
{ provide: AuditService, useValue: mock<AuditService>() },
{ provide: OrganizationService, useValue: mock<OrganizationService>() },
{
provide: PasswordStrengthServiceAbstraction,
useValue: mock<PasswordStrengthServiceAbstraction>(),
},
{ provide: PasswordHealthService, useValue: passwordHealthServiceMock },
{
provide: ActivatedRoute,
useValue: {
paramMap: of(activeRouteParams),
url: of([]),
},
},
{
provide: MemberCipherDetailsApiService,
useValue: mock<MemberCipherDetailsApiService>(),
},
{
provide: ApiService,
useValue: mock<ApiService>(),
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PasswordHealthMembersURIComponent);
component = fixture.componentInstance;
});
it("should initialize component", () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,110 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
// eslint-disable-next-line no-restricted-imports
import {
MemberCipherDetailsApiService,
PasswordHealthService,
} from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeModule,
BadgeVariant,
ContainerComponent,
TableDataSource,
TableModule,
} from "@bitwarden/components";
// eslint-disable-next-line no-restricted-imports
import { HeaderModule } from "../../layouts/header/header.module";
// eslint-disable-next-line no-restricted-imports
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
@Component({
standalone: true,
selector: "tools-password-health-members-uri",
templateUrl: "password-health-members-uri.component.html",
imports: [
BadgeModule,
CommonModule,
ContainerComponent,
PipesModule,
JslibModule,
HeaderModule,
TableModule,
],
providers: [PasswordHealthService, MemberCipherDetailsApiService],
})
export class PasswordHealthMembersURIComponent implements OnInit {
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
weakPasswordCiphers: CipherView[] = [];
passwordUseMap = new Map<string, number>();
exposedPasswordMap = new Map<string, number>();
totalMembersMap = new Map<string, number>();
dataSource = new TableDataSource<CipherView>();
reportCiphers: (CipherView & { hostURI: string })[] = [];
reportCipherURIs: string[] = [];
organization: Organization;
loading = true;
private destroyRef = inject(DestroyRef);
constructor(
protected cipherService: CipherService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected organizationService: OrganizationService,
protected auditService: AuditService,
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected memberCipherDetailsApiService: MemberCipherDetailsApiService,
) {}
ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
const organizationId = params.get("organizationId");
await this.setCiphers(organizationId);
}),
)
.subscribe();
}
async setCiphers(organizationId: string) {
const passwordHealthService = new PasswordHealthService(
this.passwordStrengthService,
this.auditService,
this.cipherService,
this.memberCipherDetailsApiService,
organizationId,
);
await passwordHealthService.generateReport();
this.dataSource.data = passwordHealthService.groupCiphersByLoginUri();
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;
this.passwordUseMap = passwordHealthService.passwordUseMap;
this.totalMembersMap = passwordHealthService.totalMembersMap;
this.loading = false;
}
}

View File

@@ -1,64 +0,0 @@
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-flex tw-flex-col" *ngIf="!loading && dataSource.data.length">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "totalMembers" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async; trackBy: trackByFunction">
<td bitCell>
<input
bitCheckbox
type="checkbox"
[checked]="selectedIds.has(r.id)"
(change)="onCheckboxChange(r.id, $event)"
/>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
<td bitCell class="tw-text-right" data-testid="total-membership">
{{ totalMembersMap.get(r.id) || 0 }}
</td>
</tr>
</ng-template>
</bit-table>
</div>

View File

@@ -1,130 +0,0 @@
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormsModule } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { debounceTime, map } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import {
MemberCipherDetailsApiService,
PasswordHealthService,
} from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeVariant,
SearchModule,
TableDataSource,
TableModule,
ToastService,
} from "@bitwarden/components";
import { HeaderModule } from "../../layouts/header/header.module";
// eslint-disable-next-line no-restricted-imports
import { SharedModule } from "../../shared";
// eslint-disable-next-line no-restricted-imports
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
@Component({
standalone: true,
selector: "tools-password-health-members",
templateUrl: "password-health-members.component.html",
imports: [PipesModule, HeaderModule, SearchModule, FormsModule, SharedModule, TableModule],
providers: [PasswordHealthService, MemberCipherDetailsApiService],
})
export class PasswordHealthMembersComponent implements OnInit {
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
passwordUseMap = new Map<string, number>();
exposedPasswordMap = new Map<string, number>();
totalMembersMap = new Map<string, number>();
dataSource = new TableDataSource<CipherView>();
loading = true;
selectedIds: Set<number> = new Set<number>();
protected searchControl = new FormControl("", { nonNullable: true });
private destroyRef = inject(DestroyRef);
constructor(
protected cipherService: CipherService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected auditService: AuditService,
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected toastService: ToastService,
protected memberCipherDetailsApiService: MemberCipherDetailsApiService,
) {
this.searchControl.valueChanges
.pipe(debounceTime(200), takeUntilDestroyed())
.subscribe((v) => (this.dataSource.filter = v));
}
ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
const organizationId = params.get("organizationId");
await this.setCiphers(organizationId);
}),
)
.subscribe();
}
async setCiphers(organizationId: string) {
const passwordHealthService = new PasswordHealthService(
this.passwordStrengthService,
this.auditService,
this.cipherService,
this.memberCipherDetailsApiService,
organizationId,
);
await passwordHealthService.generateReport();
this.dataSource.data = passwordHealthService.reportCiphers;
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;
this.passwordUseMap = passwordHealthService.passwordUseMap;
this.totalMembersMap = passwordHealthService.totalMembersMap;
this.loading = false;
}
markAppsAsCritical = async () => {
// TODO: Send to API once implemented
return new Promise((resolve) => {
setTimeout(() => {
this.selectedIds.clear();
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("appsMarkedAsCritical"),
});
resolve(true);
}, 1000);
});
};
trackByFunction(_: number, item: CipherView) {
return item.id;
}
onCheckboxChange(id: number, event: Event) {
const isChecked = (event.target as HTMLInputElement).checked;
if (isChecked) {
this.selectedIds.add(id);
} else {
this.selectedIds.delete(id);
}
}
}

View File

@@ -1,57 +0,0 @@
<bit-container>
<p>{{ "passwordsReportDesc" | i18n }}</p>
<div *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
</div>
<div class="tw-mt-4" *ngIf="!loading && dataSource.data.length">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "weakness" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesReused" | i18n }}</th>
<th bitCell class="tw-text-right">{{ "timesExposed" | i18n }}</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr bitRow *ngFor="let r of rows$ | async">
<td bitCell>
<app-vault-icon [cipher]="r"></app-vault-icon>
</td>
<td bitCell>
<ng-container>
<span>{{ r.name }}</span>
</ng-container>
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell class="tw-text-right">
<span
bitBadge
*ngIf="passwordStrengthMap.has(r.id)"
[variant]="passwordStrengthMap.get(r.id)[1]"
>
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="passwordUseMap.has(r.login.password)" variant="warning">
{{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }}
</span>
</td>
<td bitCell class="tw-text-right">
<span bitBadge *ngIf="exposedPasswordMap.has(r.id)" variant="warning">
{{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }}
</span>
</td>
</tr>
</ng-template>
</bit-table>
</div>
</bit-container>

View File

@@ -1,70 +0,0 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ActivatedRoute, convertToParamMap } from "@angular/router";
import { mock } from "jest-mock-extended";
import { of } from "rxjs";
// eslint-disable-next-line no-restricted-imports
import {
MemberCipherDetailsApiService,
PasswordHealthService,
} from "@bitwarden/bit-common/tools/reports/risk-insights";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TableModule } from "@bitwarden/components";
import { TableBodyDirective } from "@bitwarden/components/src/table/table.component";
import { LooseComponentsModule } from "../../shared";
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
import { PasswordHealthComponent } from "./password-health.component";
describe("PasswordHealthComponent", () => {
let component: PasswordHealthComponent;
let fixture: ComponentFixture<PasswordHealthComponent>;
const activeRouteParams = convertToParamMap({ organizationId: "orgId" });
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule],
declarations: [TableBodyDirective],
providers: [
{ provide: CipherService, useValue: mock<CipherService>() },
{ provide: I18nService, useValue: mock<I18nService>() },
{ provide: AuditService, useValue: mock<AuditService>() },
{ provide: ApiService, useValue: mock<ApiService>() },
{ provide: MemberCipherDetailsApiService, useValue: mock<MemberCipherDetailsApiService>() },
{
provide: PasswordStrengthServiceAbstraction,
useValue: mock<PasswordStrengthServiceAbstraction>(),
},
{
provide: PasswordHealthService,
useValue: mock<PasswordHealthService>(),
},
{
provide: ActivatedRoute,
useValue: {
paramMap: of(activeRouteParams),
url: of([]),
},
},
],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(PasswordHealthComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should initialize component", () => {
expect(component).toBeTruthy();
});
it("should call generateReport on init", () => {});
});

View File

@@ -1,100 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
// eslint-disable-next-line no-restricted-imports
import {
MemberCipherDetailsApiService,
PasswordHealthService,
} from "@bitwarden/bit-common/tools/reports/risk-insights";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeModule,
BadgeVariant,
ContainerComponent,
TableDataSource,
TableModule,
} from "@bitwarden/components";
// eslint-disable-next-line no-restricted-imports
import { HeaderModule } from "../../layouts/header/header.module";
// eslint-disable-next-line no-restricted-imports
import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module";
// eslint-disable-next-line no-restricted-imports
import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module";
@Component({
standalone: true,
selector: "tools-password-health",
templateUrl: "password-health.component.html",
imports: [
BadgeModule,
OrganizationBadgeModule,
CommonModule,
ContainerComponent,
PipesModule,
JslibModule,
HeaderModule,
TableModule,
],
providers: [PasswordHealthService, MemberCipherDetailsApiService],
})
export class PasswordHealthComponent implements OnInit {
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
passwordUseMap = new Map<string, number>();
exposedPasswordMap = new Map<string, number>();
dataSource = new TableDataSource<CipherView>();
loading = true;
private destroyRef = inject(DestroyRef);
constructor(
protected cipherService: CipherService,
protected passwordStrengthService: PasswordStrengthServiceAbstraction,
protected auditService: AuditService,
protected i18nService: I18nService,
protected activatedRoute: ActivatedRoute,
protected memberCipherDetailsApiService: MemberCipherDetailsApiService,
) {}
ngOnInit() {
this.activatedRoute.paramMap
.pipe(
takeUntilDestroyed(this.destroyRef),
map(async (params) => {
const organizationId = params.get("organizationId");
await this.setCiphers(organizationId);
}),
)
.subscribe();
}
async setCiphers(organizationId: string) {
const passwordHealthService = new PasswordHealthService(
this.passwordStrengthService,
this.auditService,
this.cipherService,
this.memberCipherDetailsApiService,
organizationId,
);
await passwordHealthService.generateReport();
this.dataSource.data = passwordHealthService.reportCiphers;
this.exposedPasswordMap = passwordHealthService.exposedPasswordMap;
this.passwordStrengthMap = passwordHealthService.passwordStrengthMap;
this.passwordUseMap = passwordHealthService.passwordUseMap;
this.loading = false;
}
}

View File

@@ -1,49 +0,0 @@
<div class="tw-mb-1 text-primary" bitTypography="body1">{{ "accessIntelligence" | i18n }}</div>
<h1 bitTypography="h1">{{ "riskInsights" | i18n }}</h1>
<div class="tw-text-muted tw-max-w-4xl tw-mb-2">
{{ "reviewAtRiskPasswords" | i18n }}
&nbsp;<a class="text-primary" routerLink="/login">{{ "learnMore" | i18n }}</a>
</div>
<div *ngIf="apps.length" class="tw-bg-primary-100 tw-rounded-lg tw-w-full tw-px-8 tw-py-2 tw-my-4">
<i class="bwi bwi-exclamation-triangle bwi-lg tw-text-[1.2rem] text-muted" aria-hidden="true"></i>
<span class="tw-mx-4">{{
"dataLastUpdated" | i18n: (dataLastUpdated | date: "MMMM d, y 'at' h:mm a")
}}</span>
<a
bitButton
buttonType="unstyled"
class="tw-border-none !tw-font-normal tw-cursor-pointer"
[bitAction]="refreshData.bind(this)"
>
{{ "refresh" | i18n }}
</a>
</div>
<bit-tab-group [(selectedIndex)]="tabIndex" (selectedIndexChange)="onTabChange($event)">
<bit-tab label="{{ 'allApplicationsWithCount' | i18n: apps.length }}">
<tools-all-applications></tools-all-applications>
</bit-tab>
<bit-tab *ngIf="isCritialAppsFeatureEnabled">
<ng-template bitTabLabel>
<i class="bwi bwi-star"></i>
{{ "criticalApplicationsWithCount" | i18n: criticalApps.length }}
</ng-template>
<tools-critical-applications></tools-critical-applications>
</bit-tab>
<bit-tab label="Raw Data">
<tools-password-health></tools-password-health>
</bit-tab>
<bit-tab label="Raw Data + members">
<tools-password-health-members></tools-password-health-members>
</bit-tab>
<bit-tab label="Raw Data + uri">
<tools-password-health-members-uri></tools-password-health-members-uri>
</bit-tab>
<!-- <bit-tab>
<ng-template bitTabLabel>
<i class="bwi bwi-envelope"></i>
{{ "notifiedMembersWithCount" | i18n: priorityApps.length }}
</ng-template>
<h2 bitTypography="h2">{{ "notifiedMembers" | i18n }}</h2>
<tools-notified-members-table></tools-notified-members-table>
</bit-tab> -->
</bit-tab-group>

View File

@@ -1,86 +0,0 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components";
import { HeaderModule } from "../../layouts/header/header.module";
import { AllApplicationsComponent } from "./all-applications.component";
import { CriticalApplicationsComponent } from "./critical-applications.component";
import { NotifiedMembersTableComponent } from "./notified-members-table.component";
import { PasswordHealthMembersURIComponent } from "./password-health-members-uri.component";
import { PasswordHealthMembersComponent } from "./password-health-members.component";
import { PasswordHealthComponent } from "./password-health.component";
export enum RiskInsightsTabType {
AllApps = 0,
CriticalApps = 1,
NotifiedMembers = 2,
}
@Component({
standalone: true,
templateUrl: "./risk-insights.component.html",
imports: [
AllApplicationsComponent,
AsyncActionsModule,
ButtonModule,
CommonModule,
CriticalApplicationsComponent,
JslibModule,
HeaderModule,
PasswordHealthComponent,
PasswordHealthMembersComponent,
PasswordHealthMembersURIComponent,
NotifiedMembersTableComponent,
TabsModule,
],
})
export class RiskInsightsComponent implements OnInit {
tabIndex: RiskInsightsTabType;
dataLastUpdated = new Date();
isCritialAppsFeatureEnabled = false;
apps: any[] = [];
criticalApps: any[] = [];
notifiedMembers: any[] = [];
async refreshData() {
// TODO: Implement
return new Promise((resolve) =>
setTimeout(() => {
this.dataLastUpdated = new Date();
resolve(true);
}, 1000),
);
}
onTabChange = async (newIndex: number) => {
await this.router.navigate([], {
relativeTo: this.route,
queryParams: { tabIndex: newIndex },
queryParamsHandling: "merge",
});
};
async ngOnInit() {
this.isCritialAppsFeatureEnabled = await this.configService.getFeatureFlag(
FeatureFlag.CriticalApps,
);
}
constructor(
protected route: ActivatedRoute,
private router: Router,
private configService: ConfigService,
) {
route.queryParams.pipe(takeUntilDestroyed()).subscribe(({ tabIndex }) => {
this.tabIndex = !isNaN(tabIndex) ? tabIndex : RiskInsightsTabType.AllApps;
});
}
}

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