[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.accessSecretsManager = u.accessSecretsManager;
view.hasMasterPassword = u.hasMasterPassword;
return view;
});

View File

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

View File

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

View File

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

View File

@ -12,13 +12,23 @@ import { BulkUserDetails } from "./bulk-status.component";
})
export class BulkRemoveComponent {
@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();
loading = false;
done = false;
error: string;
showNoMasterPasswordWarning = false;
constructor(
protected apiService: ApiService,

View File

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

View File

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

View File

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

View File

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

View File

@ -546,7 +546,7 @@ export class PeopleComponent
? "removeUserConfirmationKeyConnector"
: "removeOrgUserConfirmation";
return await this.dialogService.openSimpleDialog({
const confirmed = await this.dialogService.openSimpleDialog({
title: {
key: "removeUserIdAccess",
placeholders: [this.userNamePipe.transform(user)],
@ -554,6 +554,35 @@ export class PeopleComponent
content: { key: content },
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(
@ -608,4 +637,17 @@ export class PeopleComponent
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;
}
async revoke(user: UserType) {
const confirmed = await this.dialogService.openSimpleDialog({
protected async revokeUserConfirmationDialog(user: UserType) {
return this.dialogService.openSimpleDialog({
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
content: this.revokeWarningMessage(),
acceptButtonText: { key: "revokeAccess" },
type: SimpleDialogType.WARNING,
});
}
async revoke(user: UserType) {
const confirmed = await this.revokeUserConfirmationDialog(user);
if (!confirmed) {
return false;

View File

@ -6884,5 +6884,23 @@
},
"loginRequestApproved": {
"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;
permissions: PermissionsApi;
resetPasswordEnrolled: boolean;
hasMasterPassword: boolean;
collections: SelectionReadOnlyResponse[] = [];
groups: string[] = [];
@ -28,6 +29,7 @@ export class OrganizationUserResponse extends BaseResponse {
this.accessAll = this.getResponseProperty("AccessAll");
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
const collections = this.getResponseProperty("Collections");
if (collections != null) {