1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-21 11:35:34 +01:00

[AC-1144] Warn admins when removing or revoking users without master password (#5494)

* [AC-1144] Added new messages for warning removing/revoking user without master password

* [AC-1144] Added property 'hasMasterPassword' to OrganizationUserUserDetailsResponse and OrganizationUserView

* [AC-1144] Added user's name to 'No master password' warning

* [AC-1144] Added property 'hasMasterPassword' to ProviderUserResponse

* [AC-1144] Added alert to bulk "remove/revoke users" action when a selected user has no master password

* [AC-1144] Moved 'noMasterPasswordConfirmationDialog' method to BasePeopleComponent

* [AC-1144] Removed await from noMasterPasswordConfirmationDialog

* [AC-1144] Changed ApiService.getProviderUser to output ProviderUserUserDetailsResponse

* [AC-1144] Added warning on removing a provider user without master password

* [AC-1144] Added "No Master password" warning to provider users

* [AC-1144] Added "no master password" warning when removing/revoking user in modal view

* [AC-1144] Reverted changes made to ProviderUsers

* [AC-1144] Converted showNoMasterPasswordWarning() into a property

* [AC-1144] Fixed issue when opening invite member modal
This commit is contained in:
Rui Tomé 2023-06-16 16:38:55 +01:00 committed by GitHub
parent 1052f00b87
commit d3d17f1496
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 149 additions and 8 deletions

View File

@ -83,6 +83,7 @@ export class UserAdminService {
})); }));
view.groups = u.groups; view.groups = u.groups;
view.accessSecretsManager = u.accessSecretsManager; view.accessSecretsManager = u.accessSecretsManager;
view.hasMasterPassword = u.hasMasterPassword;
return view; return view;
}); });

View File

@ -16,6 +16,7 @@ export class OrganizationUserAdminView {
accessAll: boolean; accessAll: boolean;
permissions: PermissionsApi; permissions: PermissionsApi;
resetPasswordEnrolled: boolean; resetPasswordEnrolled: boolean;
hasMasterPassword: boolean;
collections: CollectionAccessSelectionView[] = []; collections: CollectionAccessSelectionView[] = [];
groups: string[] = []; groups: string[] = [];

View File

@ -20,6 +20,7 @@ export class OrganizationUserView {
avatarColor: string; avatarColor: string;
twoFactorEnabled: boolean; twoFactorEnabled: boolean;
usesKeyConnector: boolean; usesKeyConnector: boolean;
hasMasterPassword: boolean;
collections: CollectionAccessSelectionView[] = []; collections: CollectionAccessSelectionView[] = [];
groups: string[] = []; groups: string[] = [];

View File

@ -23,12 +23,16 @@
</app-callout> </app-callout>
<ng-container *ngIf="!done"> <ng-container *ngIf="!done">
<app-callout type="warning" *ngIf="users.length > 0 && !error"> <app-callout type="warning" *ngIf="users.length > 0 && !error">
{{ removeUsersWarning }} <p>{{ removeUsersWarning }}</p>
<p *ngIf="this.showNoMasterPasswordWarning">
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
</p>
</app-callout> </app-callout>
<table class="table table-hover table-list"> <table class="table table-hover table-list">
<thead> <thead>
<tr> <tr>
<th colspan="2">{{ "user" | i18n }}</th> <th colspan="2">{{ "user" | i18n }}</th>
<th *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
</tr> </tr>
</thead> </thead>
<tr *ngFor="let user of users"> <tr *ngFor="let user of users">
@ -39,6 +43,15 @@
{{ user.email }} {{ user.email }}
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small> <small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
</td> </td>
<td *ngIf="this.showNoMasterPasswordWarning">
<span class="text-muted d-block tw-lowercase">
<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> </tr>
</table> </table>
</ng-container> </ng-container>

View File

@ -12,13 +12,23 @@ import { BulkUserDetails } from "./bulk-status.component";
}) })
export class BulkRemoveComponent { export class BulkRemoveComponent {
@Input() organizationId: string; @Input() organizationId: string;
@Input() users: BulkUserDetails[]; @Input() set users(value: BulkUserDetails[]) {
this._users = value;
this.showNoMasterPasswordWarning = this._users.some((u) => u.hasMasterPassword === false);
}
get users(): BulkUserDetails[] {
return this._users;
}
private _users: BulkUserDetails[];
statuses: Map<string, string> = new Map(); statuses: Map<string, string> = new Map();
loading = false; loading = false;
done = false; done = false;
error: string; error: string;
showNoMasterPasswordWarning = false;
constructor( constructor(
protected apiService: ApiService, protected apiService: ApiService,

View File

@ -23,12 +23,16 @@
</app-callout> </app-callout>
<ng-container *ngIf="!done"> <ng-container *ngIf="!done">
<app-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking"> <app-callout type="warning" *ngIf="users.length > 0 && !error && isRevoking">
{{ "revokeUsersWarning" | i18n }} <p>{{ "revokeUsersWarning" | i18n }}</p>
<p *ngIf="this.showNoMasterPasswordWarning">
{{ "removeMembersWithoutMasterPasswordWarning" | i18n }}
</p>
</app-callout> </app-callout>
<table class="table table-hover table-list"> <table class="table table-hover table-list">
<thead> <thead>
<tr> <tr>
<th colspan="2">{{ "user" | i18n }}</th> <th colspan="2">{{ "user" | i18n }}</th>
<th *ngIf="this.showNoMasterPasswordWarning">{{ "details" | i18n }}</th>
</tr> </tr>
</thead> </thead>
<tr *ngFor="let user of users"> <tr *ngFor="let user of users">
@ -39,6 +43,15 @@
{{ user.email }} {{ user.email }}
<small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small> <small class="text-muted d-block" *ngIf="user.name">{{ user.name }}</small>
</td> </td>
<td *ngIf="this.showNoMasterPasswordWarning">
<span class="text-muted d-block tw-lowercase">
<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> </tr>
</table> </table>
</ng-container> </ng-container>

View File

@ -20,6 +20,7 @@ export class BulkRestoreRevokeComponent {
loading = false; loading = false;
done = false; done = false;
error: string; error: string;
showNoMasterPasswordWarning = false;
constructor( constructor(
protected i18nService: I18nService, protected i18nService: I18nService,
@ -29,6 +30,7 @@ export class BulkRestoreRevokeComponent {
this.isRevoking = config.data.isRevoking; this.isRevoking = config.data.isRevoking;
this.organizationId = config.data.organizationId; this.organizationId = config.data.organizationId;
this.users = config.data.users; this.users = config.data.users;
this.showNoMasterPasswordWarning = this.users.some((u) => u.hasMasterPassword === false);
} }
get bulkTitle() { get bulkTitle() {

View File

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

View File

@ -72,6 +72,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
canUseCustomPermissions: boolean; canUseCustomPermissions: boolean;
PermissionMode = PermissionMode; PermissionMode = PermissionMode;
canUseSecretsManager: boolean; canUseSecretsManager: boolean;
showNoMasterPasswordWarning = false;
protected organization: Organization; protected organization: Organization;
protected collectionAccessItems: AccessItemView[] = []; protected collectionAccessItems: AccessItemView[] = [];
@ -179,6 +180,9 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
throw new Error("Could not find user to edit."); throw new Error("Could not find user to edit.");
} }
this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked; this.isRevoked = userDetails.status === OrganizationUserStatusType.Revoked;
this.showNoMasterPasswordWarning =
userDetails.status > OrganizationUserStatusType.Invited &&
userDetails.hasMasterPassword === false;
const assignedCollectionsPermissions = { const assignedCollectionsPermissions = {
editAssignedCollections: userDetails.permissions.editAssignedCollections, editAssignedCollections: userDetails.permissions.editAssignedCollections,
deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections, deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections,
@ -366,7 +370,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
? "removeUserConfirmationKeyConnector" ? "removeUserConfirmationKeyConnector"
: "removeOrgUserConfirmation"; : "removeOrgUserConfirmation";
const confirmed = await this.dialogService.openSimpleDialog({ let confirmed = await this.dialogService.openSimpleDialog({
title: { key: "removeUserIdAccess", placeholders: [this.params.name] }, title: { key: "removeUserIdAccess", placeholders: [this.params.name] },
content: { key: message }, content: { key: message },
type: SimpleDialogType.WARNING, type: SimpleDialogType.WARNING,
@ -376,6 +380,14 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
return false; return false;
} }
if (this.showNoMasterPasswordWarning) {
confirmed = await this.noMasterPasswordConfirmationDialog();
if (!confirmed) {
return false;
}
}
await this.organizationUserService.deleteOrganizationUser( await this.organizationUserService.deleteOrganizationUser(
this.params.organizationId, this.params.organizationId,
this.params.organizationUserId this.params.organizationUserId
@ -394,7 +406,7 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
return; return;
} }
const confirmed = await this.dialogService.openSimpleDialog({ let confirmed = await this.dialogService.openSimpleDialog({
title: { key: "revokeUserId", placeholders: [this.params.name] }, title: { key: "revokeUserId", placeholders: [this.params.name] },
content: { key: "revokeUserConfirmation" }, content: { key: "revokeUserConfirmation" },
acceptButtonText: { key: "revokeAccess" }, acceptButtonText: { key: "revokeAccess" },
@ -405,6 +417,14 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
return false; return false;
} }
if (this.showNoMasterPasswordWarning) {
confirmed = await this.noMasterPasswordConfirmationDialog();
if (!confirmed) {
return false;
}
}
await this.organizationUserService.revokeOrganizationUser( await this.organizationUserService.revokeOrganizationUser(
this.params.organizationId, this.params.organizationId,
this.params.organizationUserId this.params.organizationUserId
@ -450,6 +470,19 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
private close(result: MemberDialogResult) { private close(result: MemberDialogResult) {
this.dialogRef.close(result); this.dialogRef.close(result);
} }
private noMasterPasswordConfirmationDialog() {
return this.dialogService.openSimpleDialog({
title: {
key: "removeOrgUserNoMasterPasswordTitle",
},
content: {
key: "removeOrgUserNoMasterPasswordDesc",
placeholders: [this.params.name],
},
type: SimpleDialogType.WARNING,
});
}
} }
function mapCollectionToAccessItemView( function mapCollectionToAccessItemView(

View File

@ -546,7 +546,7 @@ export class PeopleComponent
? "removeUserConfirmationKeyConnector" ? "removeUserConfirmationKeyConnector"
: "removeOrgUserConfirmation"; : "removeOrgUserConfirmation";
return await this.dialogService.openSimpleDialog({ const confirmed = await this.dialogService.openSimpleDialog({
title: { title: {
key: "removeUserIdAccess", key: "removeUserIdAccess",
placeholders: [this.userNamePipe.transform(user)], placeholders: [this.userNamePipe.transform(user)],
@ -554,6 +554,35 @@ export class PeopleComponent
content: { key: content }, content: { key: content },
type: SimpleDialogType.WARNING, type: SimpleDialogType.WARNING,
}); });
if (!confirmed) {
return false;
}
if (user.status > OrganizationUserStatusType.Invited && user.hasMasterPassword === false) {
return await this.noMasterPasswordConfirmationDialog(user);
}
return true;
}
protected async revokeUserConfirmationDialog(user: OrganizationUserView) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
content: this.revokeWarningMessage(),
acceptButtonText: { key: "revokeAccess" },
type: SimpleDialogType.WARNING,
});
if (!confirmed) {
return false;
}
if (user.status > OrganizationUserStatusType.Invited && user.hasMasterPassword === false) {
return await this.noMasterPasswordConfirmationDialog(user);
}
return true;
} }
private async showBulkStatus( private async showBulkStatus(
@ -608,4 +637,17 @@ export class PeopleComponent
modal.close(); modal.close();
} }
} }
private async noMasterPasswordConfirmationDialog(user: OrganizationUserView) {
return this.dialogService.openSimpleDialog({
title: {
key: "removeOrgUserNoMasterPasswordTitle",
},
content: {
key: "removeOrgUserNoMasterPasswordDesc",
placeholders: [this.userNamePipe.transform(user)],
},
type: SimpleDialogType.WARNING,
});
}
} }

View File

@ -247,13 +247,17 @@ export abstract class BasePeopleComponent<
this.actionPromise = null; this.actionPromise = null;
} }
async revoke(user: UserType) { protected async revokeUserConfirmationDialog(user: UserType) {
const confirmed = await this.dialogService.openSimpleDialog({ return this.dialogService.openSimpleDialog({
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] }, title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
content: this.revokeWarningMessage(), content: this.revokeWarningMessage(),
acceptButtonText: { key: "revokeAccess" }, acceptButtonText: { key: "revokeAccess" },
type: SimpleDialogType.WARNING, type: SimpleDialogType.WARNING,
}); });
}
async revoke(user: UserType) {
const confirmed = await this.revokeUserConfirmationDialog(user);
if (!confirmed) { if (!confirmed) {
return false; return false;

View File

@ -6884,5 +6884,23 @@
}, },
"loginRequestApproved": { "loginRequestApproved": {
"message": "Login request approved" "message": "Login request approved"
},
"removeOrgUserNoMasterPasswordTitle": {
"message": "Account does not have master password"
},
"removeOrgUserNoMasterPasswordDesc": {
"message": "Removing $USER$ without setting a master password for them may restrict access to their full account. Are you sure you want to continue?",
"placeholders": {
"user": {
"content": "$1",
"example": "John Smith"
}
}
},
"noMasterPassword": {
"message": "No master password"
},
"removeMembersWithoutMasterPasswordWarning": {
"message": "Removing members who do not have master passwords without setting one for them may restrict access to their full account."
} }
} }

View File

@ -14,6 +14,7 @@ export class OrganizationUserResponse extends BaseResponse {
accessSecretsManager: boolean; accessSecretsManager: boolean;
permissions: PermissionsApi; permissions: PermissionsApi;
resetPasswordEnrolled: boolean; resetPasswordEnrolled: boolean;
hasMasterPassword: boolean;
collections: SelectionReadOnlyResponse[] = []; collections: SelectionReadOnlyResponse[] = [];
groups: string[] = []; groups: string[] = [];
@ -28,6 +29,7 @@ export class OrganizationUserResponse extends BaseResponse {
this.accessAll = this.getResponseProperty("AccessAll"); this.accessAll = this.getResponseProperty("AccessAll");
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager"); this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled"); this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
const collections = this.getResponseProperty("Collections"); const collections = this.getResponseProperty("Collections");
if (collections != null) { if (collections != null) {