From 97e195cd7b45613d59b2a13f9e9e32958e469f3f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rui=20Tom=C3=A9?=
<108268980+r-tome@users.noreply.github.com>
Date: Thu, 17 Oct 2024 16:06:33 +0100
Subject: [PATCH] [PM-11404] Account Management: Prevent a verified user from
purging their vault (#11411)
* Update AccountService to include a method for setting the managedByOrganizationId
* Update AccountComponent to conditionally show the purgeVault button based on a feature flag and if the user is managed by an organization
* Add missing method to FakeAccountService
* Remove the setAccountManagedByOrganizationId method from the AccountService abstract class.
* Refactor AccountComponent to use OrganizationService to check for managing organization
* Rename managesActiveUser to userIsManagedByOrganization
* Refactor userIsManagedByOrganization property to be non-nullable in organization data and response models
* Refactor organization.data.spec.ts to include non-nullable userIsManagedByOrganization property
---
.../settings/account/account.component.html | 8 ++++++-
.../settings/account/account.component.ts | 22 ++++++++++++++++++-
.../models/data/organization.data.spec.ts | 1 +
.../models/data/organization.data.ts | 2 ++
.../models/domain/organization.ts | 7 ++++++
.../response/profile-organization.response.ts | 2 ++
.../src/models/response/profile.response.ts | 2 --
7 files changed, 40 insertions(+), 4 deletions(-)
diff --git a/apps/web/src/app/auth/settings/account/account.component.html b/apps/web/src/app/auth/settings/account/account.component.html
index c176469371..71508f7ae9 100644
--- a/apps/web/src/app/auth/settings/account/account.component.html
+++ b/apps/web/src/app/auth/settings/account/account.component.html
@@ -12,7 +12,13 @@
{{ "deauthorizeSessions" | i18n }}
-
+
{{ "purgeVault" | i18n }}
diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts
index 6ee7662374..dd8dc881f6 100644
--- a/apps/web/src/app/auth/settings/account/account.component.ts
+++ b/apps/web/src/app/auth/settings/account/account.component.ts
@@ -1,8 +1,11 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
-import { lastValueFrom } from "rxjs";
+import { lastValueFrom, map, Observable, of, switchMap } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { DialogService } from "@bitwarden/components";
import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.component";
@@ -19,15 +22,32 @@ export class AccountComponent implements OnInit {
deauthModalRef: ViewContainerRef;
showChangeEmail = true;
+ showPurgeVault$: Observable;
constructor(
private modalService: ModalService,
private dialogService: DialogService,
private userVerificationService: UserVerificationService,
+ private configService: ConfigService,
+ private organizationService: OrganizationService,
) {}
async ngOnInit() {
this.showChangeEmail = await this.userVerificationService.hasMasterPassword();
+ this.showPurgeVault$ = this.configService
+ .getFeatureFlag$(FeatureFlag.AccountDeprovisioning)
+ .pipe(
+ switchMap((isAccountDeprovisioningEnabled) =>
+ isAccountDeprovisioningEnabled
+ ? this.organizationService.organizations$.pipe(
+ map(
+ (organizations) =>
+ !organizations.some((o) => o.userIsManagedByOrganization === true),
+ ),
+ )
+ : of(true),
+ ),
+ );
}
async deauthorizeSessions() {
diff --git a/libs/common/src/admin-console/models/data/organization.data.spec.ts b/libs/common/src/admin-console/models/data/organization.data.spec.ts
index 4e90be7f27..0b3d512817 100644
--- a/libs/common/src/admin-console/models/data/organization.data.spec.ts
+++ b/libs/common/src/admin-console/models/data/organization.data.spec.ts
@@ -57,6 +57,7 @@ describe("ORGANIZATIONS state", () => {
limitCollectionCreationDeletion: false,
allowAdminAccessToAllCollectionItems: false,
familySponsorshipLastSyncDate: new Date(),
+ userIsManagedByOrganization: false,
},
};
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));
diff --git a/libs/common/src/admin-console/models/data/organization.data.ts b/libs/common/src/admin-console/models/data/organization.data.ts
index a5cae0af80..0c0dedad25 100644
--- a/libs/common/src/admin-console/models/data/organization.data.ts
+++ b/libs/common/src/admin-console/models/data/organization.data.ts
@@ -57,6 +57,7 @@ export class OrganizationData {
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
limitCollectionCreationDeletion: boolean;
allowAdminAccessToAllCollectionItems: boolean;
+ userIsManagedByOrganization: boolean;
constructor(
response?: ProfileOrganizationResponse,
@@ -118,6 +119,7 @@ export class OrganizationData {
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
this.limitCollectionCreationDeletion = response.limitCollectionCreationDeletion;
this.allowAdminAccessToAllCollectionItems = response.allowAdminAccessToAllCollectionItems;
+ this.userIsManagedByOrganization = response.userIsManagedByOrganization;
this.isMember = options.isMember;
this.isProviderUser = options.isProviderUser;
diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts
index 0ded0ef331..42436e0a93 100644
--- a/libs/common/src/admin-console/models/domain/organization.ts
+++ b/libs/common/src/admin-console/models/domain/organization.ts
@@ -77,6 +77,12 @@ export class Organization {
* Refers to the ability for an owner/admin to access all collection items, regardless of assigned collections
*/
allowAdminAccessToAllCollectionItems: boolean;
+ /**
+ * Indicates if this organization manages the user.
+ * A user is considered managed by an organization if their email domain
+ * matches one of the verified domains of that organization, and the user is a member of it.
+ */
+ userIsManagedByOrganization: boolean;
constructor(obj?: OrganizationData) {
if (obj == null) {
@@ -134,6 +140,7 @@ export class Organization {
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
this.limitCollectionCreationDeletion = obj.limitCollectionCreationDeletion;
this.allowAdminAccessToAllCollectionItems = obj.allowAdminAccessToAllCollectionItems;
+ this.userIsManagedByOrganization = obj.userIsManagedByOrganization;
}
get canAccess() {
diff --git a/libs/common/src/admin-console/models/response/profile-organization.response.ts b/libs/common/src/admin-console/models/response/profile-organization.response.ts
index bccb3c3cc6..4d9366e662 100644
--- a/libs/common/src/admin-console/models/response/profile-organization.response.ts
+++ b/libs/common/src/admin-console/models/response/profile-organization.response.ts
@@ -54,6 +54,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
limitCollectionCreationDeletion: boolean;
allowAdminAccessToAllCollectionItems: boolean;
+ userIsManagedByOrganization: boolean;
constructor(response: any) {
super(response);
@@ -121,5 +122,6 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.allowAdminAccessToAllCollectionItems = this.getResponseProperty(
"AllowAdminAccessToAllCollectionItems",
);
+ this.userIsManagedByOrganization = this.getResponseProperty("UserIsManagedByOrganization");
}
}
diff --git a/libs/common/src/models/response/profile.response.ts b/libs/common/src/models/response/profile.response.ts
index 24aeb11cdb..6b6555fc56 100644
--- a/libs/common/src/models/response/profile.response.ts
+++ b/libs/common/src/models/response/profile.response.ts
@@ -21,7 +21,6 @@ export class ProfileResponse extends BaseResponse {
securityStamp: string;
forcePasswordReset: boolean;
usesKeyConnector: boolean;
- managedByOrganizationId?: string | null;
organizations: ProfileOrganizationResponse[] = [];
providers: ProfileProviderResponse[] = [];
providerOrganizations: ProfileProviderOrganizationResponse[] = [];
@@ -43,7 +42,6 @@ export class ProfileResponse extends BaseResponse {
this.securityStamp = this.getResponseProperty("SecurityStamp");
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false;
this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false;
- this.managedByOrganizationId = this.getResponseProperty("ManagedByOrganizationId");
const organizations = this.getResponseProperty("Organizations");
if (organizations != null) {