1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +00:00

[PM-14613] Remove account deprovisioning feature flag (#14353)

This commit is contained in:
Thomas Rittson
2025-05-07 11:23:18 +10:00
committed by GitHub
parent 744c1b1b49
commit df40954b61
27 changed files with 153 additions and 519 deletions

View File

@@ -11,7 +11,7 @@ import {
} from "@angular/core"; } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { filter, firstValueFrom, map, Subject, takeUntil, timeout, withLatestFrom } from "rxjs"; import { filter, firstValueFrom, map, Subject, switchMap, takeUntil, timeout } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common"; import { CollectionService } from "@bitwarden/admin-console/common";
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction"; import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
@@ -29,7 +29,6 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
@@ -820,27 +819,26 @@ export class AppComponent implements OnInit, OnDestroy {
} }
private async deleteAccount() { private async deleteAccount() {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const userIsManaged = await firstValueFrom(
await firstValueFrom( this.accountService.activeAccount$.pipe(
this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning).pipe( getUserId,
withLatestFrom(this.organizationService.organizations$(userId)), switchMap((userId) => this.organizationService.organizations$(userId)),
map(async ([accountDeprovisioningEnabled, organization]) => { map((orgs) => orgs.some((o) => o.userIsManagedByOrganization === true)),
if (
accountDeprovisioningEnabled &&
organization.some((o) => o.userIsManagedByOrganization === true)
) {
await this.dialogService.openSimpleDialog({
title: { key: "cannotDeleteAccount" },
content: { key: "cannotDeleteAccountDesc" },
cancelButtonText: null,
acceptButtonText: { key: "close" },
type: "danger",
});
} else {
DeleteAccountComponent.open(this.dialogService);
}
}),
), ),
); );
if (userIsManaged) {
await this.dialogService.openSimpleDialog({
title: { key: "cannotDeleteAccount" },
content: { key: "cannotDeleteAccountDesc" },
cancelButtonText: null,
acceptButtonText: { key: "close" },
type: "danger",
});
return;
}
DeleteAccountComponent.open(this.dialogService);
} }
} }

View File

@@ -50,11 +50,15 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
} }
get showBulkConfirmUsers(): boolean { get showBulkConfirmUsers(): boolean {
return this.dataSource.acceptedUserCount > 0; return this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Accepted);
} }
get showBulkReinviteUsers(): boolean { get showBulkReinviteUsers(): boolean {
return this.dataSource.invitedUserCount > 0; return this.dataSource
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Invited);
} }
abstract userType: typeof OrganizationUserType | typeof ProviderUserType; abstract userType: typeof OrganizationUserType | typeof ProviderUserType;

View File

@@ -115,7 +115,7 @@
*ngIf="canAccessExport$ | async" *ngIf="canAccessExport$ | async"
></bit-nav-item> ></bit-nav-item>
<bit-nav-item <bit-nav-item
[text]="domainVerificationNavigationTextKey | i18n" [text]="'claimedDomains' | i18n"
route="settings/domain-verification" route="settings/domain-verification"
*ngIf="organization?.canManageDomainVerification" *ngIf="organization?.canManageDomainVerification"
></bit-nav-item> ></bit-nav-item>

View File

@@ -55,7 +55,6 @@ export class OrganizationLayoutComponent implements OnInit {
protected readonly logo = AdminConsoleLogo; protected readonly logo = AdminConsoleLogo;
protected orgFilter = (org: Organization) => canAccessOrgAdmin(org); protected orgFilter = (org: Organization) => canAccessOrgAdmin(org);
protected domainVerificationNavigationTextKey: string;
protected integrationPageEnabled$: Observable<boolean>; protected integrationPageEnabled$: Observable<boolean>;
@@ -146,12 +145,6 @@ export class OrganizationLayoutComponent implements OnInit {
this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations)); this.integrationPageEnabled$ = this.organization$.pipe(map((org) => org.canAccessIntegrations));
this.domainVerificationNavigationTextKey = (await this.configService.getFeatureFlag(
FeatureFlag.AccountDeprovisioning,
))
? "claimedDomains"
: "domainVerification";
this.canShowPoliciesTab$ = this.organization$.pipe( this.canShowPoliciesTab$ = this.organization$.pipe(
switchMap((organization) => switchMap((organization) =>
this.organizationBillingService this.organizationBillingService

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Component, Inject } from "@angular/core"; import { Component, Inject } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components"; import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
@@ -35,7 +32,6 @@ export class BulkDeleteDialogComponent {
@Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams, @Inject(DIALOG_DATA) protected dialogParams: BulkDeleteDialogParams,
protected i18nService: I18nService, protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService, private organizationUserApiService: OrganizationUserApiService,
private configService: ConfigService,
private deleteManagedMemberWarningService: DeleteManagedMemberWarningService, private deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
) { ) {
this.organizationId = dialogParams.organizationId; this.organizationId = dialogParams.organizationId;
@@ -43,11 +39,7 @@ export class BulkDeleteDialogComponent {
} }
async submit() { async submit() {
if ( await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organizationId);
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning))
) {
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organizationId);
}
try { try {
this.loading = true; this.loading = true;

View File

@@ -1,12 +1,6 @@
<bit-dialog <bit-dialog dialogSize="large">
dialogSize="large"
*ngIf="{ enabled: accountDeprovisioningEnabled$ | async } as accountDeprovisioning"
>
<ng-container bitDialogTitle> <ng-container bitDialogTitle>
<span *ngIf="accountDeprovisioning.enabled; else nonMemberTitle">{{ bulkMemberTitle }}</span> <span>{{ bulkTitle }}</span>
<ng-template #nonMemberTitle>
{{ bulkTitle }}
</ng-template>
</ng-container> </ng-container>
<div bitDialogContent> <div bitDialogContent>
@@ -20,7 +14,7 @@
<bit-callout <bit-callout
type="danger" type="danger"
*ngIf="nonCompliantMembers && accountDeprovisioning.enabled" *ngIf="nonCompliantMembers"
title="{{ 'nonCompliantMembersTitle' | i18n }}" title="{{ 'nonCompliantMembersTitle' | i18n }}"
> >
{{ "nonCompliantMembersError" | i18n }} {{ "nonCompliantMembersError" | i18n }}
@@ -50,7 +44,7 @@
<bit-table> <bit-table>
<ng-container header> <ng-container header>
<tr> <tr>
<th bitCell>{{ (accountDeprovisioning.enabled ? "member" : "user") | i18n }}</th> <th bitCell>{{ "member" | i18n }}</th>
<th bitCell class="tw-w-1/2" *ngIf="this.showNoMasterPasswordWarning"> <th bitCell class="tw-w-1/2" *ngIf="this.showNoMasterPasswordWarning">
{{ "details" | i18n }} {{ "details" | i18n }}
</th> </th>
@@ -82,7 +76,7 @@
<ng-container header> <ng-container header>
<tr> <tr>
<th bitCell class="tw-w-1/2"> <th bitCell class="tw-w-1/2">
{{ (accountDeprovisioning.enabled ? "member" : "user") | i18n }} {{ "member" | i18n }}
</th> </th>
<th bitCell class="tw-w-1/2">{{ "status" | i18n }}</th> <th bitCell class="tw-w-1/2">{{ "status" | i18n }}</th>
</tr> </tr>
@@ -113,7 +107,7 @@
[bitAction]="submit" [bitAction]="submit"
buttonType="primary" buttonType="primary"
> >
{{ accountDeprovisioning.enabled ? bulkMemberTitle : bulkTitle }} {{ bulkTitle }}
</button> </button>
<button type="button" bitButton buttonType="secondary" bitDialogClose> <button type="button" bitButton buttonType="secondary" bitDialogClose>
{{ "close" | i18n }} {{ "close" | i18n }}

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Component, Inject } from "@angular/core"; import { Component, Inject } from "@angular/core";
import { Observable } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DIALOG_DATA, DialogService } from "@bitwarden/components"; import { DIALOG_DATA, DialogService } from "@bitwarden/components";
@@ -34,12 +31,10 @@ export class BulkRestoreRevokeComponent {
error: string; error: string;
showNoMasterPasswordWarning = false; showNoMasterPasswordWarning = false;
nonCompliantMembers: boolean = false; nonCompliantMembers: boolean = false;
accountDeprovisioningEnabled$: Observable<boolean>;
constructor( constructor(
protected i18nService: I18nService, protected i18nService: I18nService,
private organizationUserApiService: OrganizationUserApiService, private organizationUserApiService: OrganizationUserApiService,
private configService: ConfigService,
@Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams, @Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams,
) { ) {
this.isRevoking = data.isRevoking; this.isRevoking = data.isRevoking;
@@ -48,17 +43,9 @@ export class BulkRestoreRevokeComponent {
this.showNoMasterPasswordWarning = this.users.some( this.showNoMasterPasswordWarning = this.users.some(
(u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false, (u) => u.status > OrganizationUserStatusType.Invited && u.hasMasterPassword === false,
); );
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
} }
get bulkTitle() { get bulkTitle() {
const titleKey = this.isRevoking ? "revokeUsers" : "restoreUsers";
return this.i18nService.t(titleKey);
}
get bulkMemberTitle() {
const titleKey = this.isRevoking ? "revokeMembers" : "restoreMembers"; const titleKey = this.isRevoking ? "revokeMembers" : "restoreMembers";
return this.i18nService.t(titleKey); return this.i18nService.t(titleKey);
} }

View File

@@ -275,11 +275,7 @@
{{ "revoke" | i18n }} {{ "revoke" | i18n }}
</button> </button>
<button <button
*ngIf=" *ngIf="this.editMode && !(editParams$ | async)?.managedByOrganization"
this.editMode &&
(!(accountDeprovisioningEnabled$ | async) ||
!(editParams$ | async)?.managedByOrganization)
"
type="button" type="button"
buttonType="danger" buttonType="danger"
bitButton bitButton
@@ -290,11 +286,7 @@
{{ "remove" | i18n }} {{ "remove" | i18n }}
</button> </button>
<button <button
*ngIf=" *ngIf="this.editMode && (editParams$ | async)?.managedByOrganization"
this.editMode &&
(accountDeprovisioningEnabled$ | async) &&
(editParams$ | async)?.managedByOrganization
"
type="button" type="button"
buttonType="danger" buttonType="danger"
bitButton bitButton

View File

@@ -152,10 +152,6 @@ export class MemberDialogComponent implements OnDestroy {
manageResetPassword: false, manageResetPassword: false,
}); });
protected accountDeprovisioningEnabled$: Observable<boolean> = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
protected isExternalIdVisible$ = this.configService protected isExternalIdVisible$ = this.configService
.getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility) .getFeatureFlag$(FeatureFlag.SsoExternalIdVisibility)
.pipe( .pipe(
@@ -667,11 +663,9 @@ export class MemberDialogComponent implements OnDestroy {
const showWarningDialog = combineLatest([ const showWarningDialog = combineLatest([
this.organization$, this.organization$,
this.deleteManagedMemberWarningService.warningAcknowledged(this.params.organizationId), this.deleteManagedMemberWarningService.warningAcknowledged(this.params.organizationId),
this.accountDeprovisioningEnabled$,
]).pipe( ]).pipe(
map( map(
([organization, acknowledged, featureFlagEnabled]) => ([organization, acknowledged]) =>
featureFlagEnabled &&
organization.canManageUsers && organization.canManageUsers &&
organization.productTierType === ProductTierType.Enterprise && organization.productTierType === ProductTierType.Enterprise &&
!acknowledged, !acknowledged,
@@ -714,9 +708,8 @@ export class MemberDialogComponent implements OnDestroy {
message: this.i18nService.t("organizationUserDeleted", this.params.name), message: this.i18nService.t("organizationUserDeleted", this.params.name),
}); });
if (await firstValueFrom(this.accountDeprovisioningEnabled$)) { await this.deleteManagedMemberWarningService.acknowledgeWarning(this.params.organizationId);
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.params.organizationId);
}
this.close(MemberDialogResult.Deleted); this.close(MemberDialogResult.Deleted);
}; };

View File

@@ -345,7 +345,7 @@
{{ "revokeAccess" | i18n }} {{ "revokeAccess" | i18n }}
</button> </button>
<button <button
*ngIf="!accountDeprovisioningEnabled || !u.managedByOrganization" *ngIf="!u.managedByOrganization"
type="button" type="button"
bitMenuItem bitMenuItem
(click)="remove(u)" (click)="remove(u)"
@@ -355,7 +355,7 @@
</span> </span>
</button> </button>
<button <button
*ngIf="accountDeprovisioningEnabled && u.managedByOrganization" *ngIf="u.managedByOrganization"
type="button" type="button"
bitMenuItem bitMenuItem
(click)="deleteUser(u)" (click)="deleteUser(u)"

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { import {
@@ -46,9 +46,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums"; import { isNotSelfUpgradable, ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
@@ -91,7 +89,7 @@ class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView>
@Component({ @Component({
templateUrl: "members.component.html", templateUrl: "members.component.html",
}) })
export class MembersComponent extends BaseMembersComponent<OrganizationUserView> implements OnInit { export class MembersComponent extends BaseMembersComponent<OrganizationUserView> {
@ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true }) @ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true })
resetPasswordModalRef: ViewContainerRef; resetPasswordModalRef: ViewContainerRef;
@@ -104,7 +102,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
status: OrganizationUserStatusType = null; status: OrganizationUserStatusType = null;
orgResetPasswordPolicyEnabled = false; orgResetPasswordPolicyEnabled = false;
orgIsOnSecretsManagerStandalone = false; orgIsOnSecretsManagerStandalone = false;
accountDeprovisioningEnabled = false;
protected canUseSecretsManager$: Observable<boolean>; protected canUseSecretsManager$: Observable<boolean>;
@@ -139,7 +136,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
private groupService: GroupApiService, private groupService: GroupApiService,
private collectionService: CollectionService, private collectionService: CollectionService,
private billingApiService: BillingApiServiceAbstraction, private billingApiService: BillingApiServiceAbstraction,
private configService: ConfigService,
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService, protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
) { ) {
super( super(
@@ -237,12 +233,6 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
.subscribe(); .subscribe();
} }
async ngOnInit() {
this.accountDeprovisioningEnabled = await this.configService.getFeatureFlag(
FeatureFlag.AccountDeprovisioning,
);
}
async getUsers(): Promise<OrganizationUserView[]> { async getUsers(): Promise<OrganizationUserView[]> {
let groupsPromise: Promise<Map<string, string>>; let groupsPromise: Promise<Map<string, string>>;
let collectionsPromise: Promise<Map<string, string>>; let collectionsPromise: Promise<Map<string, string>>;
@@ -591,20 +581,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
} }
async bulkDelete() { async bulkDelete() {
if (this.accountDeprovisioningEnabled) { const warningAcknowledged = await firstValueFrom(
const warningAcknowledged = await firstValueFrom( this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id), );
);
if ( if (
!warningAcknowledged && !warningAcknowledged &&
this.organization.canManageUsers && this.organization.canManageUsers &&
this.organization.productTierType === ProductTierType.Enterprise this.organization.productTierType === ProductTierType.Enterprise
) { ) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning(); const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) { if (!acknowledged) {
return; return;
}
} }
} }
@@ -794,20 +782,18 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
} }
async deleteUser(user: OrganizationUserView) { async deleteUser(user: OrganizationUserView) {
if (this.accountDeprovisioningEnabled) { const warningAcknowledged = await firstValueFrom(
const warningAcknowledged = await firstValueFrom( this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id),
this.deleteManagedMemberWarningService.warningAcknowledged(this.organization.id), );
);
if ( if (
!warningAcknowledged && !warningAcknowledged &&
this.organization.canManageUsers && this.organization.canManageUsers &&
this.organization.productTierType === ProductTierType.Enterprise this.organization.productTierType === ProductTierType.Enterprise
) { ) {
const acknowledged = await this.deleteManagedMemberWarningService.showWarning(); const acknowledged = await this.deleteManagedMemberWarningService.showWarning();
if (!acknowledged) { if (!acknowledged) {
return false; return false;
}
} }
} }
@@ -829,9 +815,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
return false; return false;
} }
if (this.accountDeprovisioningEnabled) { await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
await this.deleteManagedMemberWarningService.acknowledgeWarning(this.organization.id);
}
this.actionPromise = this.organizationUserApiService.deleteOrganizationUser( this.actionPromise = this.organizationUserApiService.deleteOrganizationUser(
this.organization.id, this.organization.id,
@@ -864,56 +848,23 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
}); });
} }
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 { get showBulkRestoreUsers(): boolean {
return ( return this.dataSource
!this.accountDeprovisioningEnabled || .getCheckedUsers()
this.dataSource .every((member) => member.status == this.userStatusType.Revoked);
.getCheckedUsers()
.every((member) => member.status == this.userStatusType.Revoked)
);
} }
get showBulkRevokeUsers(): boolean { get showBulkRevokeUsers(): boolean {
return ( return this.dataSource
!this.accountDeprovisioningEnabled || .getCheckedUsers()
this.dataSource .every((member) => member.status != this.userStatusType.Revoked);
.getCheckedUsers()
.every((member) => member.status != this.userStatusType.Revoked)
);
} }
get showBulkRemoveUsers(): boolean { get showBulkRemoveUsers(): boolean {
return ( return this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization);
!this.accountDeprovisioningEnabled ||
this.dataSource.getCheckedUsers().every((member) => !member.managedByOrganization)
);
} }
get showBulkDeleteUsers(): boolean { get showBulkDeleteUsers(): boolean {
if (!this.accountDeprovisioningEnabled) {
return false;
}
const validStatuses = [ const validStatuses = [
this.userStatusType.Accepted, this.userStatusType.Accepted,
this.userStatusType.Confirmed, this.userStatusType.Confirmed,

View File

@@ -1,11 +1,6 @@
<bit-callout *ngIf="accountDeprovisioningEnabled$ | async; else disabledBlock" type="warning"> <bit-callout type="warning">
{{ "singleOrgPolicyMemberWarning" | i18n }} {{ "singleOrgPolicyMemberWarning" | i18n }}
</bit-callout> </bit-callout>
<ng-template #disabledBlock>
<bit-callout type="warning">
{{ "singleOrgPolicyWarning" | i18n }}
</bit-callout>
</ng-template>
<bit-form-control> <bit-form-control>
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" /> <input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />

View File

@@ -1,15 +1,12 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { firstValueFrom, Observable } from "rxjs";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } 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 { BasePolicy, BasePolicyComponent } from "./base-policy.component"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
export class SingleOrgPolicy extends BasePolicy { export class SingleOrgPolicy extends BasePolicy {
name = "singleOrg"; name = "singleOrg";
description = "singleOrgDesc"; description = "singleOrgPolicyDesc";
type = PolicyType.SingleOrg; type = PolicyType.SingleOrg;
component = SingleOrgPolicyComponent; component = SingleOrgPolicyComponent;
} }
@@ -19,22 +16,9 @@ export class SingleOrgPolicy extends BasePolicy {
templateUrl: "single-org.component.html", templateUrl: "single-org.component.html",
}) })
export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnInit { export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnInit {
constructor(private configService: ConfigService) {
super();
}
protected accountDeprovisioningEnabled$: Observable<boolean> = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
async ngOnInit() { async ngOnInit() {
super.ngOnInit(); super.ngOnInit();
const isAccountDeprovisioningEnabled = await firstValueFrom(this.accountDeprovisioningEnabled$);
this.policy.description = isAccountDeprovisioningEnabled
? "singleOrgPolicyDesc"
: "singleOrgDesc";
if (!this.policyResponse.canToggleState) { if (!this.policyResponse.canToggleState) {
this.enabled.disable(); this.enabled.disable();
} }

View File

@@ -1,20 +1,10 @@
import { Component, OnInit, OnDestroy } from "@angular/core"; import { Component, OnInit, OnDestroy } from "@angular/core";
import { import { firstValueFrom, from, lastValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
combineLatest,
firstValueFrom,
from,
lastValueFrom,
map,
Observable,
Subject,
takeUntil,
} from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
@@ -47,10 +37,6 @@ export class AccountComponent implements OnInit, OnDestroy {
async ngOnInit() { async ngOnInit() {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const isAccountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
const userIsManagedByOrganization$ = this.organizationService const userIsManagedByOrganization$ = this.organizationService
.organizations$(userId) .organizations$(userId)
.pipe( .pipe(
@@ -61,25 +47,14 @@ export class AccountComponent implements OnInit, OnDestroy {
this.showChangeEmail$ = hasMasterPassword$; this.showChangeEmail$ = hasMasterPassword$;
this.showPurgeVault$ = combineLatest([ this.showPurgeVault$ = userIsManagedByOrganization$.pipe(
isAccountDeprovisioningEnabled$, map((userIsManagedByOrganization) => !userIsManagedByOrganization),
userIsManagedByOrganization$,
]).pipe(
map(
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
); );
this.showDeleteAccount$ = combineLatest([ this.showDeleteAccount$ = userIsManagedByOrganization$.pipe(
isAccountDeprovisioningEnabled$, map((userIsManagedByOrganization) => !userIsManagedByOrganization),
userIsManagedByOrganization$,
]).pipe(
map(
([isAccountDeprovisioningEnabled, userIsManagedByOrganization]) =>
!isAccountDeprovisioningEnabled || !userIsManagedByOrganization,
),
); );
this.accountService.accountVerifyNewDeviceLogin$ this.accountService.accountVerifyNewDeviceLogin$
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe((verifyDevices) => { .subscribe((verifyDevices) => {

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core"; import { Component } from "@angular/core";
import { Observable } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { TypographyModule } from "@bitwarden/components"; import { TypographyModule } from "@bitwarden/components";
/** /**
@@ -18,13 +15,4 @@ import { TypographyModule } from "@bitwarden/components";
standalone: true, standalone: true,
imports: [TypographyModule, JslibModule, CommonModule], imports: [TypographyModule, JslibModule, CommonModule],
}) })
export class DangerZoneComponent implements OnInit { export class DangerZoneComponent {}
constructor(private configService: ConfigService) {}
accountDeprovisioningEnabled$: Observable<boolean>;
ngOnInit(): void {
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
}
}

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms"; import { FormControl, FormGroup } from "@angular/forms";
import { firstValueFrom, map, Observable, of, Subject, switchMap, takeUntil } from "rxjs"; import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -10,9 +10,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request"; import { UpdateProfileRequest } from "@bitwarden/common/auth/models/request/update-profile.request";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response"; import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
@@ -40,7 +38,6 @@ export class ProfileComponent implements OnInit, OnDestroy {
private accountService: AccountService, private accountService: AccountService,
private dialogService: DialogService, private dialogService: DialogService,
private toastService: ToastService, private toastService: ToastService,
private configService: ConfigService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
) {} ) {}
@@ -53,21 +50,12 @@ export class ProfileComponent implements OnInit, OnDestroy {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.managingOrganization$ = this.configService this.managingOrganization$ = this.organizationService
.getFeatureFlag$(FeatureFlag.AccountDeprovisioning) .organizations$(userId)
.pipe( .pipe(
switchMap((isAccountDeprovisioningEnabled) => map((organizations) => organizations.find((o) => o.userIsManagedByOrganization === true)),
isAccountDeprovisioningEnabled
? this.organizationService
.organizations$(userId)
.pipe(
map((organizations) =>
organizations.find((o) => o.userIsManagedByOrganization === true),
),
)
: of(null),
),
); );
this.formGroup.get("name").setValue(this.profile.name); this.formGroup.get("name").setValue(this.profile.name);
this.formGroup.get("email").setValue(this.profile.email); this.formGroup.get("email").setValue(this.profile.email);

View File

@@ -9,9 +9,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { DeviceType, EventType } from "@bitwarden/common/enums"; import { DeviceType, EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EventResponse } from "@bitwarden/common/models/response/event.response"; import { EventResponse } from "@bitwarden/common/models/response/event.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@Injectable() @Injectable()
@@ -21,8 +19,7 @@ export class EventService {
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
policyService: PolicyService, policyService: PolicyService,
private configService: ConfigService, accountService: AccountService,
private accountService: AccountService,
) { ) {
accountService.activeAccount$ accountService.activeAccount$
.pipe( .pipe(
@@ -463,20 +460,10 @@ export class EventService {
msg = humanReadableMsg = this.i18nService.t("removedDomain", ev.domainName); msg = humanReadableMsg = this.i18nService.t("removedDomain", ev.domainName);
break; break;
case EventType.OrganizationDomain_Verified: case EventType.OrganizationDomain_Verified:
msg = humanReadableMsg = this.i18nService.t( msg = humanReadableMsg = this.i18nService.t("domainClaimedEvent", ev.domainName);
(await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning))
? "domainClaimedEvent"
: "domainVerifiedEvent",
ev.domainName,
);
break; break;
case EventType.OrganizationDomain_NotVerified: case EventType.OrganizationDomain_NotVerified:
msg = humanReadableMsg = this.i18nService.t( msg = humanReadableMsg = this.i18nService.t("domainNotClaimedEvent", ev.domainName);
(await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning))
? "domainNotClaimedEvent"
: "domainNotVerifiedEvent",
ev.domainName,
);
break; break;
// Secrets Manager // Secrets Manager
case EventType.Secret_Retrieved: case EventType.Secret_Retrieved:

View File

@@ -1,14 +1,5 @@
import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { import { combineLatest, map, Observable, Subject, switchMap, takeUntil } from "rxjs";
combineLatest,
firstValueFrom,
map,
Observable,
of,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import { import {
OrganizationUserApiService, OrganizationUserApiService,
@@ -25,7 +16,6 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -83,22 +73,11 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
map((policies) => policies.filter((p) => p.type == PolicyType.ResetPassword)), map((policies) => policies.filter((p) => p.type == PolicyType.ResetPassword)),
); );
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const managingOrg$ = this.accountService.activeAccount$.pipe(
const managingOrg$ = this.configService getUserId,
.getFeatureFlag$(FeatureFlag.AccountDeprovisioning) switchMap((userId) => this.organizationService.organizations$(userId)),
.pipe( map((organizations) => organizations.find((o) => o.userIsManagedByOrganization === true)),
switchMap((isAccountDeprovisioningEnabled) => );
isAccountDeprovisioningEnabled
? this.organizationService
.organizations$(userId)
.pipe(
map((organizations) =>
organizations.find((o) => o.userIsManagedByOrganization === true),
),
)
: of(null),
),
);
combineLatest([ combineLatest([
this.organization$, this.organization$,

View File

@@ -5055,9 +5055,6 @@
"singleOrgBlockCreateMessage": { "singleOrgBlockCreateMessage": {
"message": "Your current organization has a policy that does not allow you to join more than one organization. Please contact your organization admins or sign up from a different Bitwarden account." "message": "Your current organization has a policy that does not allow you to join more than one organization. Please contact your organization admins or sign up from a different Bitwarden account."
}, },
"singleOrgPolicyWarning": {
"message": "Organization members who are not owners or admins and are already a member of another organization will be removed from your organization."
},
"singleOrgPolicyMemberWarning": { "singleOrgPolicyMemberWarning": {
"message": "Non-compliant members will be placed in revoked status until they leave all other organizations. Administrators are exempt and can restore members once compliance is met." "message": "Non-compliant members will be placed in revoked status until they leave all other organizations. Administrators are exempt and can restore members once compliance is met."
}, },
@@ -5887,15 +5884,6 @@
"fingerprintPhrase": { "fingerprintPhrase": {
"message": "Fingerprint phrase:" "message": "Fingerprint phrase:"
}, },
"removeUsers": {
"message": "Remove users"
},
"revokeUsers": {
"message": "Revoke users"
},
"restoreUsers": {
"message": "Restore users"
},
"error": { "error": {
"message": "Error" "message": "Error"
}, },
@@ -7837,12 +7825,6 @@
"noDomainsSubText": { "noDomainsSubText": {
"message": "Connecting a domain allows members to skip the SSO identifier field during Login with SSO." "message": "Connecting a domain allows members to skip the SSO identifier field during Login with SSO."
}, },
"verifyDomain": {
"message": "Verify domain"
},
"reverifyDomain": {
"message": "Reverify domain"
},
"copyDnsTxtRecord": { "copyDnsTxtRecord": {
"message": "Copy DNS TXT record" "message": "Copy DNS TXT record"
}, },
@@ -7852,18 +7834,6 @@
"dnsTxtRecordInputHint": { "dnsTxtRecordInputHint": {
"message": "Copy and paste the TXT record into your DNS Provider." "message": "Copy and paste the TXT record into your DNS Provider."
}, },
"domainNameInputHint": {
"message": "Example: mydomain.com. Subdomains require separate entries to be verified."
},
"automaticDomainVerification": {
"message": "Automatic Domain Verification"
},
"automaticDomainVerificationProcess": {
"message": "Bitwarden will attempt to verify the domain 3 times during the first 72 hours. If the domain cant be verified, check the DNS record in your host and manually verify. The domain will be removed from your organization in 7 days if it is not verified"
},
"invalidDomainNameMessage": {
"message": "Input is not a valid format. Format: mydomain.com. Subdomains require separate entries to be verified."
},
"removeDomain": { "removeDomain": {
"message": "Remove domain" "message": "Remove domain"
}, },
@@ -7876,9 +7846,6 @@
"domainSaved": { "domainSaved": {
"message": "Domain saved" "message": "Domain saved"
}, },
"domainVerified": {
"message": "Domain verified"
},
"duplicateDomainError": { "duplicateDomainError": {
"message": "You can't claim the same domain twice." "message": "You can't claim the same domain twice."
}, },
@@ -7891,21 +7858,6 @@
} }
} }
}, },
"domainNotVerified": {
"message": "$DOMAIN$ not verified. Check your DNS record.",
"placeholders": {
"DOMAIN": {
"content": "$1",
"example": "bitwarden.com"
}
}
},
"domainStatusVerified": {
"message": "Verified"
},
"domainStatusUnverified": {
"message": "Unverified"
},
"domainNameTh": { "domainNameTh": {
"message": "Name" "message": "Name"
}, },
@@ -7939,24 +7891,6 @@
} }
} }
}, },
"domainVerifiedEvent": {
"message": "$DOMAIN$ verified",
"placeholders": {
"DOMAIN": {
"content": "$1",
"example": "bitwarden.com"
}
}
},
"domainNotVerifiedEvent": {
"message": "$DOMAIN$ not verified",
"placeholders": {
"DOMAIN": {
"content": "$1",
"example": "bitwarden.com"
}
}
},
"verificationRequiredForActionSetPinToContinue": { "verificationRequiredForActionSetPinToContinue": {
"message": "Verification required for this action. Set a PIN to continue." "message": "Verification required for this action. Set a PIN to continue."
}, },

View File

@@ -7,7 +7,7 @@
<span bitDialogTitle> <span bitDialogTitle>
<span *ngIf="!data.orgDomain">{{ "newDomain" | i18n }}</span> <span *ngIf="!data.orgDomain">{{ "newDomain" | i18n }}</span>
<span *ngIf="data.orgDomain"> <span *ngIf="data.orgDomain">
{{ ((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n }} {{ "claimDomain" | i18n }}
</span> </span>
<span *ngIf="data.orgDomain" class="tw-text-xs tw-text-muted"> <span *ngIf="data.orgDomain" class="tw-text-xs tw-text-muted">
@@ -15,30 +15,17 @@
</span> </span>
<span *ngIf="data?.orgDomain && !data.orgDomain?.verifiedDate" bitBadge variant="warning"> <span *ngIf="data?.orgDomain && !data.orgDomain?.verifiedDate" bitBadge variant="warning">
{{ {{ "domainStatusUnderVerification" | i18n }}
((accountDeprovisioningEnabled$ | async)
? "domainStatusUnderVerification"
: "domainStatusUnverified"
) | i18n
}}
</span> </span>
<span *ngIf="data?.orgDomain && data?.orgDomain?.verifiedDate" bitBadge variant="success"> <span *ngIf="data?.orgDomain && data?.orgDomain?.verifiedDate" bitBadge variant="success">
{{ {{ "domainStatusClaimed" | i18n }}
((accountDeprovisioningEnabled$ | async) ? "domainStatusClaimed" : "domainStatusVerified")
| i18n
}}
</span> </span>
</span> </span>
<div bitDialogContent> <div bitDialogContent>
<bit-form-field> <bit-form-field>
<bit-label>{{ "domainName" | i18n }}</bit-label> <bit-label>{{ "domainName" | i18n }}</bit-label>
<input bitInput appAutofocus formControlName="domainName" [showErrorsWhenDisabled]="true" /> <input bitInput appAutofocus formControlName="domainName" [showErrorsWhenDisabled]="true" />
<bit-hint>{{ <bit-hint>{{ "claimDomainNameInputHint" | i18n }}</bit-hint>
((accountDeprovisioningEnabled$ | async)
? "claimDomainNameInputHint"
: "domainNameInputHint"
) | i18n
}}</bit-hint>
</bit-form-field> </bit-form-field>
<bit-form-field *ngIf="data?.orgDomain"> <bit-form-field *ngIf="data?.orgDomain">
@@ -57,29 +44,18 @@
<bit-callout <bit-callout
*ngIf="data?.orgDomain && !data?.orgDomain?.verifiedDate" *ngIf="data?.orgDomain && !data?.orgDomain?.verifiedDate"
type="info" type="info"
title="{{ title="{{ 'automaticClaimedDomains' | i18n | uppercase }}"
(accountDeprovisioningEnabled$ | async)
? ('automaticClaimedDomains' | i18n | uppercase)
: ('automaticDomainVerification' | i18n)
}}"
> >
{{ {{ "automaticDomainClaimProcess" | i18n }}
((accountDeprovisioningEnabled$ | async)
? "automaticDomainClaimProcess"
: "automaticDomainVerificationProcess"
) | i18n
}}
</bit-callout> </bit-callout>
</div> </div>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>
<button type="submit" bitButton bitFormButton buttonType="primary"> <button type="submit" bitButton bitFormButton buttonType="primary">
<span *ngIf="!data?.orgDomain">{{ "next" | i18n }}</span> <span *ngIf="!data?.orgDomain">{{ "next" | i18n }}</span>
<span *ngIf="data?.orgDomain && !data?.orgDomain?.verifiedDate">{{ <span *ngIf="data?.orgDomain && !data?.orgDomain?.verifiedDate">{{
((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain") | i18n "claimDomain" | i18n
}}</span>
<span *ngIf="data?.orgDomain?.verifiedDate">{{
((accountDeprovisioningEnabled$ | async) ? "reclaimDomain" : "reverifyDomain") | i18n
}}</span> }}</span>
<span *ngIf="data?.orgDomain?.verifiedDate">{{ "reclaimDomain" | i18n }}</span>
</button> </button>
<button bitButton buttonType="secondary" (click)="dialogRef.close()" type="button"> <button bitButton buttonType="secondary" (click)="dialogRef.close()" type="button">
{{ "cancel" | i18n }} {{ "cancel" | i18n }}

View File

@@ -2,19 +2,15 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from "@angular/forms"; import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from "@angular/forms";
import { Subject, takeUntil, Observable, firstValueFrom } from "rxjs"; import { Subject, takeUntil } from "rxjs";
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
import { OrgDomainServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain.service.abstraction"; import { OrgDomainServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain.service.abstraction";
import { OrganizationDomainResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain.response"; import { OrganizationDomainResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain.response";
import { OrganizationDomainRequest } from "@bitwarden/common/admin-console/services/organization-domain/requests/organization-domain.request"; import { OrganizationDomainRequest } from "@bitwarden/common/admin-console/services/organization-domain/requests/organization-domain.request";
import { HttpStatusCode } from "@bitwarden/common/enums"; import { HttpStatusCode } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { DialogRef, DIALOG_DATA, DialogService, ToastService } from "@bitwarden/components"; import { DialogRef, DIALOG_DATA, DialogService, ToastService } from "@bitwarden/components";
@@ -32,7 +28,6 @@ export interface DomainAddEditDialogData {
export class DomainAddEditDialogComponent implements OnInit, OnDestroy { export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
private componentDestroyed$: Subject<void> = new Subject(); private componentDestroyed$: Subject<void> = new Subject();
accountDeprovisioningEnabled$: Observable<boolean>;
domainForm: FormGroup; domainForm: FormGroup;
get domainNameCtrl(): FormControl { get domainNameCtrl(): FormControl {
@@ -50,20 +45,13 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
public dialogRef: DialogRef, public dialogRef: DialogRef,
@Inject(DIALOG_DATA) public data: DomainAddEditDialogData, @Inject(DIALOG_DATA) public data: DomainAddEditDialogData,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private cryptoFunctionService: CryptoFunctionServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
private orgDomainApiService: OrgDomainApiServiceAbstraction, private orgDomainApiService: OrgDomainApiServiceAbstraction,
private orgDomainService: OrgDomainServiceAbstraction, private orgDomainService: OrgDomainServiceAbstraction,
private validationService: ValidationService, private validationService: ValidationService,
private dialogService: DialogService, private dialogService: DialogService,
private toastService: ToastService, private toastService: ToastService,
private configService: ConfigService, ) {}
) {
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
}
// Angular Method Implementations // Angular Method Implementations
@@ -73,11 +61,7 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
"", "",
[ [
Validators.required, Validators.required,
domainNameValidator( domainNameValidator(this.i18nService.t("invalidDomainNameClaimMessage")),
(await firstValueFrom(this.accountDeprovisioningEnabled$))
? this.i18nService.t("invalidDomainNameClaimMessage")
: this.i18nService.t("invalidDomainNameMessage"),
),
uniqueInArrayValidator( uniqueInArrayValidator(
this.data.existingDomainNames, this.data.existingDomainNames,
this.i18nService.t("duplicateDomainError"), this.i18nService.t("duplicateDomainError"),
@@ -223,22 +207,13 @@ export class DomainAddEditDialogComponent implements OnInit, OnDestroy {
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
title: null, title: null,
message: this.i18nService.t( message: this.i18nService.t("domainClaimed"),
(await firstValueFrom(this.accountDeprovisioningEnabled$))
? "domainClaimed"
: "domainVerified",
),
}); });
this.dialogRef.close(); this.dialogRef.close();
} else { } else {
this.domainNameCtrl.setErrors({ this.domainNameCtrl.setErrors({
errorPassthrough: { errorPassthrough: {
message: this.i18nService.t( message: this.i18nService.t("domainNotClaimed", this.domainNameCtrl.value),
(await firstValueFrom(this.accountDeprovisioningEnabled$))
? "domainNotClaimed"
: "domainNotVerified",
this.domainNameCtrl.value,
),
}, },
}); });
// For the case where user opens dialog and reverifies when domain name formControl disabled. // For the case where user opens dialog and reverifies when domain name formControl disabled.

View File

@@ -4,11 +4,7 @@
</button> </button>
</app-header> </app-header>
<p <p bitTypography="body1" class="tw-text-main tw-w-2/5">
bitTypography="body1"
class="tw-text-main tw-w-2/5"
*ngIf="accountDeprovisioningEnabled$ | async"
>
{{ "claimedDomainsDesc" | i18n }} {{ "claimedDomainsDesc" | i18n }}
<a <a
bitLink bitLink
@@ -58,16 +54,10 @@
</td> </td>
<td bitCell> <td bitCell>
<span *ngIf="!orgDomain?.verifiedDate" bitBadge variant="warning">{{ <span *ngIf="!orgDomain?.verifiedDate" bitBadge variant="warning">{{
((accountDeprovisioningEnabled$ | async) "domainStatusUnderVerification" | i18n
? "domainStatusUnderVerification"
: "domainStatusUnverified"
) | i18n
}}</span> }}</span>
<span *ngIf="orgDomain?.verifiedDate" bitBadge variant="success">{{ <span *ngIf="orgDomain?.verifiedDate" bitBadge variant="success">{{
((accountDeprovisioningEnabled$ | async) "domainStatusClaimed" | i18n
? "domainStatusClaimed"
: "domainStatusVerified"
) | i18n
}}</span> }}</span>
</td> </td>
<td bitCell class="tw-text-muted"> <td bitCell class="tw-text-muted">
@@ -94,10 +84,7 @@
type="button" type="button"
> >
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i> <i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ {{ "claimDomain" | i18n }}
((accountDeprovisioningEnabled$ | async) ? "claimDomain" : "verifyDomain")
| i18n
}}
</button> </button>
<button bitMenuItem (click)="deleteDomain(orgDomain.id)" type="button"> <button bitMenuItem (click)="deleteDomain(orgDomain.id)" type="button">
<span class="tw-text-danger"> <span class="tw-text-danger">

View File

@@ -11,7 +11,6 @@ import {
switchMap, switchMap,
take, take,
takeUntil, takeUntil,
withLatestFrom,
} from "rxjs"; } from "rxjs";
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction";
@@ -22,7 +21,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { HttpStatusCode } from "@bitwarden/common/enums"; import { HttpStatusCode } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -46,7 +44,6 @@ export class DomainVerificationComponent implements OnInit, OnDestroy {
organizationId: string; organizationId: string;
orgDomains$: Observable<OrganizationDomainResponse[]>; orgDomains$: Observable<OrganizationDomainResponse[]>;
accountDeprovisioningEnabled$: Observable<boolean>;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -59,11 +56,7 @@ export class DomainVerificationComponent implements OnInit, OnDestroy {
private configService: ConfigService, private configService: ConfigService,
private policyService: PolicyService, private policyService: PolicyService,
private accountService: AccountService, private accountService: AccountService,
) { ) {}
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
}
async ngOnInit() { async ngOnInit() {
this.orgDomains$ = this.orgDomainService.orgDomains$; this.orgDomains$ = this.orgDomainService.orgDomains$;
@@ -85,20 +78,18 @@ export class DomainVerificationComponent implements OnInit, OnDestroy {
async load() { async load() {
await this.orgDomainApiService.getAllByOrgId(this.organizationId); await this.orgDomainApiService.getAllByOrgId(this.organizationId);
if (await this.configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning)) { const singleOrgPolicy = await firstValueFrom(
const singleOrgPolicy = await firstValueFrom( this.accountService.activeAccount$.pipe(
this.accountService.activeAccount$.pipe( getUserId,
getUserId, switchMap((userId) => this.policyService.policies$(userId)),
switchMap((userId) => this.policyService.policies$(userId)), map((policies) =>
map((policies) => policies.find(
policies.find( (p) => p.type === PolicyType.SingleOrg && p.organizationId === this.organizationId,
(p) => p.type === PolicyType.SingleOrg && p.organizationId === this.organizationId,
),
), ),
), ),
); ),
this.singleOrgPolicyEnabled = singleOrgPolicy?.enabled ?? false; );
} this.singleOrgPolicyEnabled = singleOrgPolicy?.enabled ?? false;
this.loading = false; this.loading = false;
} }
@@ -110,29 +101,30 @@ export class DomainVerificationComponent implements OnInit, OnDestroy {
existingDomainNames: this.getExistingDomainNames(), existingDomainNames: this.getExistingDomainNames(),
}; };
await firstValueFrom( const showSingleOrgWarning = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning).pipe( this.orgDomains$.pipe(
withLatestFrom(this.orgDomains$), map(
map(async ([accountDeprovisioningEnabled, organizationDomains]) => { (organizationDomains) =>
if (
accountDeprovisioningEnabled &&
!this.singleOrgPolicyEnabled && !this.singleOrgPolicyEnabled &&
organizationDomains.every((domain) => domain.verifiedDate === null) organizationDomains.every((domain) => domain.verifiedDate === null),
) { ),
await this.dialogService.openSimpleDialog({
title: { key: "claim-domain-single-org-warning" },
content: { key: "single-org-revoked-user-warning" },
cancelButtonText: { key: "cancel" },
acceptButtonText: { key: "confirm" },
acceptAction: () => this.openAddDomainDialog(domainAddEditDialogData),
type: "info",
});
} else {
await this.openAddDomainDialog(domainAddEditDialogData);
}
}),
), ),
); );
if (showSingleOrgWarning) {
await this.dialogService.openSimpleDialog({
title: { key: "claim-domain-single-org-warning" },
content: { key: "single-org-revoked-user-warning" },
cancelButtonText: { key: "cancel" },
acceptButtonText: { key: "confirm" },
acceptAction: () => this.openAddDomainDialog(domainAddEditDialogData),
type: "info",
});
return;
}
await this.openAddDomainDialog(domainAddEditDialogData);
} }
private async openAddDomainDialog(domainAddEditDialogData: DomainAddEditDialogData) { private async openAddDomainDialog(domainAddEditDialogData: DomainAddEditDialogData) {
@@ -184,22 +176,13 @@ export class DomainVerificationComponent implements OnInit, OnDestroy {
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
title: null, title: null,
message: this.i18nService.t( message: this.i18nService.t("domainClaimed"),
(await firstValueFrom(this.accountDeprovisioningEnabled$))
? "domainClaimed"
: "domainVerified",
),
}); });
} else { } else {
this.toastService.showToast({ this.toastService.showToast({
variant: "error", variant: "error",
title: null, title: null,
message: this.i18nService.t( message: this.i18nService.t("domainNotClaimed", domainName),
(await firstValueFrom(this.accountDeprovisioningEnabled$))
? "domainNotClaimed"
: "domainNotVerified",
domainName,
),
}); });
// Update this item so the last checked date gets updated. // Update this item so the last checked date gets updated.
await this.updateOrgDomain(orgDomainId); await this.updateOrgDomain(orgDomainId);

View File

@@ -1,10 +1,8 @@
import { inject, NgModule } from "@angular/core"; import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from "@angular/router";
import { authGuard } from "@bitwarden/angular/auth/guards"; import { authGuard } from "@bitwarden/angular/auth/guards";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { isEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard"; import { isEnterpriseOrgGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/is-enterprise-org.guard";
import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard"; import { organizationPermissionsGuard } from "@bitwarden/web-vault/app/admin-console/organizations/guards/org-permissions.guard";
import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component"; import { OrganizationLayoutComponent } from "@bitwarden/web-vault/app/admin-console/organizations/layouts/organization-layout.component";
@@ -30,12 +28,7 @@ const routes: Routes = [
component: DomainVerificationComponent, component: DomainVerificationComponent,
canActivate: [organizationPermissionsGuard((org) => org.canManageDomainVerification)], canActivate: [organizationPermissionsGuard((org) => org.canManageDomainVerification)],
resolve: { resolve: {
titleId: async () => { titleId: "claimedDomains",
const configService = inject(ConfigService);
return (await configService.getFeatureFlag(FeatureFlag.AccountDeprovisioning))
? "claimedDomains"
: "domainVerification";
},
}, },
}, },
{ {

View File

@@ -31,10 +31,7 @@
<input bitInput type="text" formControlName="ssoIdentifier" /> <input bitInput type="text" formControlName="ssoIdentifier" />
<bit-hint> <bit-hint>
{{ "ssoIdentifierHintPartOne" | i18n }} {{ "ssoIdentifierHintPartOne" | i18n }}
<a bitLink routerLink="../domain-verification">{{ <a bitLink routerLink="../domain-verification">{{ "claimedDomains" | i18n }}</a>
((accountDeprovisioningEnabled$ | async) ? "claimedDomains" : "domainVerification")
| i18n
}}</a>
</bit-hint> </bit-hint>
</bit-form-field> </bit-form-field>

View File

@@ -9,7 +9,7 @@ import {
Validators, Validators,
} from "@angular/forms"; } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { concatMap, firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs";
import { ControlsOf } from "@bitwarden/angular/types/controls-of"; import { ControlsOf } from "@bitwarden/angular/types/controls-of";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -33,8 +33,6 @@ import { OrganizationSsoRequest } from "@bitwarden/common/auth/models/request/or
import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response"; import { OrganizationSsoResponse } from "@bitwarden/common/auth/models/response/organization-sso.response";
import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view"; import { SsoConfigView } from "@bitwarden/common/auth/models/view/sso-config.view";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -191,8 +189,6 @@ export class SsoComponent implements OnInit, OnDestroy {
return this.ssoConfigForm?.controls?.configType as FormControl; return this.ssoConfigForm?.controls?.configType as FormControl;
} }
accountDeprovisioningEnabled$: Observable<boolean>;
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -202,13 +198,8 @@ export class SsoComponent implements OnInit, OnDestroy {
private organizationService: OrganizationService, private organizationService: OrganizationService,
private accountService: AccountService, private accountService: AccountService,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private configService: ConfigService,
private toastService: ToastService, private toastService: ToastService,
) { ) {}
this.accountDeprovisioningEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.AccountDeprovisioning,
);
}
async ngOnInit() { async ngOnInit() {
this.enabledCtrl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((enabled) => { this.enabledCtrl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((enabled) => {

View File

@@ -9,7 +9,6 @@ import { ServerConfig } from "../platform/abstractions/config/server-config";
*/ */
export enum FeatureFlag { export enum FeatureFlag {
/* Admin Console Team */ /* Admin Console Team */
AccountDeprovisioning = "pm-10308-account-deprovisioning",
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission", LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission",
SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility", SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility",
@@ -80,7 +79,6 @@ const FALSE = false as boolean;
*/ */
export const DefaultFeatureFlagValue = { export const DefaultFeatureFlagValue = {
/* Admin Console Team */ /* Admin Console Team */
[FeatureFlag.AccountDeprovisioning]: FALSE,
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
[FeatureFlag.LimitItemDeletion]: FALSE, [FeatureFlag.LimitItemDeletion]: FALSE,
[FeatureFlag.SsoExternalIdVisibility]: FALSE, [FeatureFlag.SsoExternalIdVisibility]: FALSE,