[PM-12066] Add sorting to weak password report (#11027)

* Simplify the filter(toggle group) to filter by organizationId instead of a orgFilterStatus property which is not present on the CipherView

* Add sorting to weak password report table

- Create new type to represent a row within the report
- Add types and remove usage of any
- Include the score/badge within the data passed to the datasource/table instead of looking it up via the `passwordStrengthMap`
- Remove unneeded passwordStrengthCache
-  Enable sorting via bitSortable
- Set default sort to order by weakness

* Show headers and sort also within AC version of weak-password report, but hide the Owner column

* Clarify that we are filtering by OrgId

* Use a typed object for the reportValue instead of an array

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Daniel James Smith 2024-09-27 01:38:18 +02:00 committed by GitHub
parent eb7eb614f5
commit 9eeaf0a61f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 61 additions and 78 deletions

View File

@ -5,6 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
@ -81,7 +82,7 @@ export class CipherReportComponent implements OnDestroy {
if (filterId === 0) {
cipherCount = this.allCiphers.length;
} else if (filterId === 1) {
cipherCount = this.allCiphers.filter((c: any) => c.orgFilterStatus === null).length;
cipherCount = this.allCiphers.filter((c) => c.organizationId === null).length;
} else {
this.organizations.filter((org: Organization) => {
if (org.id === filterId) {
@ -89,22 +90,20 @@ export class CipherReportComponent implements OnDestroy {
return org;
}
});
cipherCount = this.allCiphers.filter(
(c: any) => c.orgFilterStatus === orgFilterStatus,
).length;
cipherCount = this.allCiphers.filter((c) => c.organizationId === orgFilterStatus).length;
}
return cipherCount;
}
async filterOrgToggle(status: any) {
this.currentFilterStatus = status;
if (status === 0) {
this.dataSource.filter = null;
} else if (status === 1) {
this.dataSource.filter = (c: any) => c.orgFilterStatus == null;
} else {
this.dataSource.filter = (c: any) => c.orgFilterStatus === status;
let filter = null;
if (typeof status === "number" && status === 1) {
filter = (c: CipherView) => c.organizationId == null;
} else if (typeof status === "string") {
const orgId = status as OrganizationId;
filter = (c: CipherView) => c.organizationId === orgId;
}
this.dataSource.filter = filter;
}
async load() {
@ -183,9 +182,7 @@ export class CipherReportComponent implements OnDestroy {
protected filterCiphersByOrg(ciphersList: CipherView[]) {
this.allCiphers = [...ciphersList];
this.ciphers = ciphersList.map((ciph: any) => {
ciph.orgFilterStatus = ciph.organizationId;
this.ciphers = ciphersList.map((ciph) => {
if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) {
this.filterStatus.push(ciph.organizationId);
} else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) {
@ -193,7 +190,6 @@ export class CipherReportComponent implements OnDestroy {
}
return ciph;
});
this.dataSource.data = this.ciphers;
if (this.filterStatus.length > 2) {

View File

@ -32,12 +32,14 @@
</ng-container>
</bit-toggle-group>
<bit-table [dataSource]="dataSource">
<ng-container header *ngIf="!isAdminConsoleActive">
<ng-container header>
<tr bitRow>
<th bitCell></th>
<th bitCell>{{ "name" | i18n }}</th>
<th bitCell>{{ "owner" | i18n }}</th>
<th bitCell></th>
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
<th bitCell bitSortable="organizationId" *ngIf="!isAdminConsoleActive">
{{ "owner" | i18n }}
</th>
<th bitCell class="tw-text-right" bitSortable="reportValue" default></th>
</tr>
</ng-container>
<ng-template body let-rows$>
@ -80,7 +82,7 @@
<br />
<small>{{ r.subTitle }}</small>
</td>
<td bitCell>
<td bitCell *ngIf="!isAdminConsoleActive">
<app-org-badge
*ngIf="!organization"
[disabled]="disabled"
@ -91,8 +93,8 @@
</app-org-badge>
</td>
<td bitCell class="tw-text-right">
<span bitBadge [variant]="passwordStrengthMap.get(r.id)[1]">
{{ passwordStrengthMap.get(r.id)[0] | i18n }}
<span bitBadge [variant]="r.reportValue.badgeVariant">
{{ r.reportValue.label | i18n }}
</span>
</td>
</tr>

View File

@ -14,16 +14,17 @@ import { PasswordRepromptService } from "@bitwarden/vault";
import { CipherReportComponent } from "./cipher-report.component";
type ReportScore = { label: string; badgeVariant: BadgeVariant };
type ReportResult = CipherView & { reportValue: ReportScore };
@Component({
selector: "app-weak-passwords-report",
templateUrl: "weak-passwords-report.component.html",
})
export class WeakPasswordsReportComponent extends CipherReportComponent implements OnInit {
passwordStrengthMap = new Map<string, [string, BadgeVariant]>();
disabled = true;
private passwordStrengthCache = new Map<string, number>();
weakPasswordCiphers: CipherView[] = [];
weakPasswordCiphers: ReportResult[] = [];
constructor(
protected cipherService: CipherService,
@ -49,16 +50,15 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
}
async setCiphers() {
const allCiphers: any = await this.getAllCiphers();
this.passwordStrengthCache = new Map<string, number>();
const allCiphers = await this.getAllCiphers();
this.weakPasswordCiphers = [];
this.filterStatus = [0];
this.findWeakPasswords(allCiphers);
}
protected findWeakPasswords(ciphers: any[]): void {
protected findWeakPasswords(ciphers: CipherView[]): void {
ciphers.forEach((ciph) => {
const { type, login, isDeleted, edit, viewPassword, id } = ciph;
const { type, login, isDeleted, edit, viewPassword } = ciph;
if (
type !== CipherType.Login ||
login.password == null ||
@ -71,50 +71,39 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
}
const hasUserName = this.isUserNameNotEmpty(ciph);
const cacheKey = this.getCacheKey(ciph);
if (!this.passwordStrengthCache.has(cacheKey)) {
let userInput: string[] = [];
if (hasUserName) {
const atPosition = login.username.indexOf("@");
if (atPosition > -1) {
userInput = userInput
.concat(
login.username
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/),
)
.filter((i) => i.length >= 3);
} else {
userInput = login.username
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
.filter((i: any) => i.length >= 3);
}
let userInput: string[] = [];
if (hasUserName) {
const atPosition = login.username.indexOf("@");
if (atPosition > -1) {
userInput = userInput
.concat(
login.username
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/),
)
.filter((i) => i.length >= 3);
} else {
userInput = login.username
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
.filter((i) => i.length >= 3);
}
const result = this.passwordStrengthService.getPasswordStrength(
login.password,
null,
userInput.length > 0 ? userInput : null,
);
this.passwordStrengthCache.set(cacheKey, result.score);
}
const score = this.passwordStrengthCache.get(cacheKey);
if (score != null && score <= 2) {
this.passwordStrengthMap.set(id, this.scoreKey(score));
this.weakPasswordCiphers.push(ciph);
}
});
this.weakPasswordCiphers.sort((a, b) => {
return (
this.passwordStrengthCache.get(this.getCacheKey(a)) -
this.passwordStrengthCache.get(this.getCacheKey(b))
const result = this.passwordStrengthService.getPasswordStrength(
login.password,
null,
userInput.length > 0 ? userInput : null,
);
});
if (result.score != null && result.score <= 2) {
const scoreValue = this.scoreKey(result.score);
const row = { ...ciph, reportValue: scoreValue } as ReportResult;
this.weakPasswordCiphers.push(row);
}
});
this.filterCiphersByOrg(this.weakPasswordCiphers);
}
@ -127,20 +116,16 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
return !Utils.isNullOrWhitespace(c.login.username);
}
private getCacheKey(c: CipherView): string {
return c.login.password + "_____" + (this.isUserNameNotEmpty(c) ? c.login.username : "");
}
private scoreKey(score: number): [string, BadgeVariant] {
private scoreKey(score: number): ReportScore {
switch (score) {
case 4:
return ["strong", "success"];
return { label: "strong", badgeVariant: "success" };
case 3:
return ["good", "primary"];
return { label: "good", badgeVariant: "primary" };
case 2:
return ["weak", "warning"];
return { label: "weak", badgeVariant: "warning" };
default:
return ["veryWeak", "danger"];
return { label: "veryWeak", badgeVariant: "danger" };
}
}
}