From a530e8853e686ada0d8db4db7d679f688d7e57a1 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Fri, 11 Oct 2024 16:45:49 -0700 Subject: [PATCH 1/9] add raw data component --- .../access-intelligence-routing.module.ts | 3 +- .../access-intelligence.component.html | 7 +- .../access-intelligence.component.ts | 2 + .../password-health.component.html | 90 ++++++ .../password-health.component.spec.ts | 123 +++++++++ .../password-health.component.ts | 258 ++++++++++++++++++ .../password-health.mock.ts | 66 +++++ ...exposed-passwords-report.component.spec.ts | 2 +- .../reports/pages/reports-ciphers.mock.ts | 44 +++ .../reused-passwords-report.component.spec.ts | 2 +- .../weak-passwords-report.component.spec.ts | 2 +- 11 files changed, 592 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.component.html create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.component.ts create mode 100644 apps/web/src/app/tools/access-intelligence/password-health.mock.ts diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts b/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts index b35b1fa64a3..88efb2b4832 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence-routing.module.ts @@ -1,7 +1,6 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { unauthGuardFn } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -11,7 +10,7 @@ const routes: Routes = [ { path: "", component: AccessIntelligenceComponent, - canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence), unauthGuardFn()], + canActivate: [canAccessFeature(FeatureFlag.AccessIntelligence)], data: { titleId: "accessIntelligence", }, diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html index 665f8f6b0c5..df3eee389f6 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.html @@ -1,6 +1,9 @@ - + + + + diff --git a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts index 9e5eff6f629..8bdaadbd7e4 100644 --- a/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts +++ b/apps/web/src/app/tools/access-intelligence/access-intelligence.component.ts @@ -11,6 +11,7 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { ApplicationTableComponent } from "./application-table.component"; import { NotifiedMembersTableComponent } from "./notified-members-table.component"; +import { PasswordHealthComponent } from "./password-health.component"; export enum AccessIntelligenceTabType { AllApps = 0, @@ -26,6 +27,7 @@ export enum AccessIntelligenceTabType { CommonModule, JslibModule, HeaderModule, + PasswordHealthComponent, NotifiedMembersTableComponent, TabsModule, ], diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.html b/apps/web/src/app/tools/access-intelligence/password-health.component.html new file mode 100644 index 00000000000..c6d424f0433 --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.html @@ -0,0 +1,90 @@ + +

{{ "passwordsReportDesc" | i18n }}

+
+ + {{ "loading" | i18n }} +
+
+ + + + + {{ "name" | i18n }} + {{ "organization" | i18n }} + {{ "weakness" | i18n }} + {{ "timesReused" | i18n }} + {{ "timesExposed" | i18n }} + + + + + + + + + + {{ + r.name + }} + + + {{ r.name }} + + + + {{ "shared" | i18n }} + + + + {{ "attachments" | i18n }} + +
+ {{ r.subTitle }} + + + + + + + + {{ passwordStrengthMap.get(r.id)[0] | i18n }} + + + + + {{ "reusedXTimes" | i18n: passwordUseMap.get(r.login.password) }} + + + + + {{ "exposedXTimes" | i18n: exposedPasswordMap.get(r.id) }} + + + +
+
+
+
diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts new file mode 100644 index 00000000000..15e3c705914 --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { + TableBodyDirective, + TableComponent, +} from "@bitwarden/components/src/table/table.component"; +import { PasswordRepromptService } from "@bitwarden/vault"; +// eslint-disable-next-line no-restricted-imports +import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; + +// eslint-disable-next-line no-restricted-imports +import { PasswordsReportComponent } from "../reports/pages/passwords-report.component"; +// eslint-disable-next-line no-restricted-imports +import { userData } from "../reports/pages/passwords-report.mock"; +// eslint-disable-next-line no-restricted-imports +import { cipherData } from "../reports/pages/reports-ciphers.mock"; + +describe("PasswordsReportComponent", () => { + let component: PasswordsReportComponent; + let fixture: ComponentFixture; + let passwordStrengthService: MockProxy; + let organizationService: MockProxy; + let syncServiceMock: MockProxy; + let cipherServiceMock: MockProxy; + let auditServiceMock: MockProxy; + + beforeEach(async () => { + passwordStrengthService = mock(); + auditServiceMock = mock(); + organizationService = mock(); + syncServiceMock = mock(); + cipherServiceMock = mock(); + + organizationService.organizations$ = of([]); + + await TestBed.configureTestingModule({ + imports: [PipesModule], + declarations: [PasswordsReportComponent, I18nPipe, TableComponent, TableBodyDirective], + providers: [ + { provide: CipherService, useValue: cipherServiceMock }, + { provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService }, + { provide: AuditService, useValue: auditServiceMock }, + { provide: OrganizationService, useValue: organizationService }, + { provide: ModalService, useValue: mock() }, + { provide: PasswordRepromptService, useValue: mock() }, + { provide: SyncService, useValue: syncServiceMock }, + { provide: I18nService, useValue: mock() }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordsReportComponent); + component = fixture.componentInstance; + + (component as any).cipherData = cipherData; + (component as any).userData = userData; + + fixture.detectChanges(); + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); + + it("should populate reportCiphers with ciphers that have password issues", async () => { + passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 1 } as any); + + auditServiceMock.passwordLeaked.mockResolvedValue(5); + + await component.setCiphers(); + + const cipherIds = component.reportCiphers.map((c) => c.id); + + expect(cipherIds).toEqual([ + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001227nm6", + "cbea34a8-bde4-46ad-9d19-b05001227nm7", + ]); + expect(component.reportCiphers.length).toEqual(4); + }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); + + it("should correctly populate passwordStrengthMap", async () => { + passwordStrengthService.getPasswordStrength.mockImplementation((password) => { + let score = 0; + if (password === "123") { + score = 1; + } else { + score = 4; + } + return { score } as any; + }); + + auditServiceMock.passwordLeaked.mockResolvedValue(0); + + await component.setCiphers(); + + expect(component.passwordStrengthMap.size).toBeGreaterThan(0); + expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toEqual([ + "veryWeak", + "danger", + ]); + expect(component.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toEqual([ + "veryWeak", + "danger", + ]); + }); +}); diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.ts new file mode 100644 index 00000000000..5b0f55fd3a4 --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.ts @@ -0,0 +1,258 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { BadgeModule, BadgeVariant, ContainerComponent, TableModule } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +// eslint-disable-next-line no-restricted-imports +import { HeaderModule } from "../../layouts/header/header.module"; +// eslint-disable-next-line no-restricted-imports +import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module"; +// eslint-disable-next-line no-restricted-imports +import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; +// eslint-disable-next-line no-restricted-imports +import { CipherReportComponent } from "../reports/pages/cipher-report.component"; + +@Component({ + standalone: true, + selector: "tools-password-health", + templateUrl: "password-health.component.html", + imports: [ + BadgeModule, + OrganizationBadgeModule, + CommonModule, + ContainerComponent, + PipesModule, + JslibModule, + HeaderModule, + TableModule, + ], +}) +export class PasswordHealthComponent extends CipherReportComponent implements OnInit { + passwordStrengthMap = new Map(); + + private passwordStrengthCache = new Map(); + weakPasswordCiphers: CipherView[] = []; + + reusedPasswordCiphers: CipherView[] = []; + passwordUseMap: Map; + + exposedPasswordCiphers: CipherView[] = []; + exposedPasswordMap = new Map(); + + reportCiphers: CipherView[] = []; + reportCipherIds: string[] = []; + + constructor( + protected cipherService: CipherService, + protected passwordStrengthService: PasswordStrengthServiceAbstraction, + protected organizationService: OrganizationService, + protected auditService: AuditService, + modalService: ModalService, + passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, + syncService: SyncService, + ) { + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); + } + + async ngOnInit() { + await super.load(); + } + + async setCiphers() { + const allCiphers = await this.getAllCiphers(); + this.filterStatus = [0]; + this.setWeakPasswordMap(allCiphers); + this.setReusedPasswordMap(allCiphers); + await this.setExposedPasswordMap(allCiphers); + + // const reportIssues = allCiphers.map((c) => { + // if (this.passwordStrengthMap.has(c.id)) { + // return c; + // } + + // if (this.passwordUseMap.has(c.id)) { + // return c; + // } + + // if (this.exposedPasswordMap.has(c.id)) { + // return c; + // } + // }); + + this.filterCiphersByOrg(this.reportCiphers); + } + + protected setWeakPasswordMap(ciphers: any[]) { + this.weakPasswordCiphers = []; + this.filterStatus = [0]; + this.findWeakPasswords(ciphers); + } + + protected async setExposedPasswordMap(ciphers: any[]) { + const promises: Promise[] = []; + + ciphers.forEach((ciph: any) => { + const { type, login, isDeleted, edit, viewPassword, id } = ciph; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => { + if (exposedCount > 0) { + this.exposedPasswordCiphers.push(ciph); + this.exposedPasswordMap.set(id, exposedCount); + if (!this.reportCipherIds.includes(ciph.id)) { + this.reportCipherIds.push(ciph.id); + this.reportCiphers.push(ciph); + } + } + }); + promises.push(promise); + }); + await Promise.all(promises); + } + + protected setReusedPasswordMap(ciphers: any[]): void { + const ciphersWithPasswords: CipherView[] = []; + this.passwordUseMap = new Map(); + + ciphers.forEach((ciph) => { + const { type, login, isDeleted, edit, viewPassword } = ciph; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + ciphersWithPasswords.push(ciph); + if (this.passwordUseMap.has(login.password)) { + this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1); + } else { + this.passwordUseMap.set(login.password, 1); + } + }); + this.reusedPasswordCiphers = ciphersWithPasswords.filter( + (c) => + (this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password)) || + 0 > 1, + ); + } + + protected findWeakPasswords(ciphers: any[]): void { + ciphers.forEach((ciph) => { + const { type, login, isDeleted, edit, viewPassword, id } = ciph; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } + + 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); + } + } + 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)) || 0) - + (this.passwordStrengthCache.get(this.getCacheKey(b)) || 0) + ); + }); + } + + protected canManageCipher(c: CipherView): boolean { + // this will only ever be false from the org view; + return true; + } + + private isUserNameNotEmpty(c: CipherView): boolean { + 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] { + switch (score) { + case 4: + return ["strong", "success"]; + case 3: + return ["good", "primary"]; + case 2: + return ["weak", "warning"]; + default: + return ["veryWeak", "danger"]; + } + } +} diff --git a/apps/web/src/app/tools/access-intelligence/password-health.mock.ts b/apps/web/src/app/tools/access-intelligence/password-health.mock.ts new file mode 100644 index 00000000000..d01edc37a59 --- /dev/null +++ b/apps/web/src/app/tools/access-intelligence/password-health.mock.ts @@ -0,0 +1,66 @@ +export const userData: any[] = [ + { + userName: "David Brent", + email: "david.brent@wernhamhogg.uk", + usesKeyConnector: true, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab1", + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + "cbea34a8-bde4-46ad-9d19-b05001227nm5", + ], + }, + { + userName: "Tim Canterbury", + email: "tim.canterbury@wernhamhogg.uk", + usesKeyConnector: false, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + "cbea34a8-bde4-46ad-9d19-b05001227nm5", + ], + }, + { + userName: "Gareth Keenan", + email: "gareth.keenan@wernhamhogg.uk", + usesKeyConnector: true, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + "cbea34a8-bde4-46ad-9d19-b05001227nm5", + "cbea34a8-bde4-46ad-9d19-b05001227nm7", + ], + }, + { + userName: "Dawn Tinsley", + email: "dawn.tinsley@wernhamhogg.uk", + usesKeyConnector: true, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + ], + }, + { + userName: "Keith Bishop", + email: "keith.bishop@wernhamhogg.uk", + usesKeyConnector: false, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab1", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + "cbea34a8-bde4-46ad-9d19-b05001227nm5", + ], + }, + { + userName: "Chris Finch", + email: "chris.finch@wernhamhogg.uk", + usesKeyConnector: true, + cipherIds: [ + "cbea34a8-bde4-46ad-9d19-b05001228ab2", + "cbea34a8-bde4-46ad-9d19-b05001228cd3", + "cbea34a8-bde4-46ad-9d19-b05001228xy4", + ], + }, +]; diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index 07dc218bd64..ea2f7c7d516 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -83,7 +83,7 @@ describe("ExposedPasswordsReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(2); + expect(component.ciphers.length).toEqual(4); expect(component.ciphers[0].id).toEqual(expectedIdOne); expect(component.ciphers[0].edit).toEqual(true); expect(component.ciphers[1].id).toEqual(expectedIdTwo); diff --git a/apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts b/apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts index 195c9afa9d2..0b594165718 100644 --- a/apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts +++ b/apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts @@ -125,4 +125,48 @@ export const cipherData: any[] = [ reprompt: 0, localData: null, }, + { + initializerKey: 1, + id: "cbea34a8-bde4-46ad-9d19-b05001227nm6", + organizationId: null, + folderId: null, + name: "Can Be Edited id ending 6", + notes: null, + type: 1, + favorite: false, + organizationUseTotp: false, + login: { + password: "TestPass123", + }, + edit: true, + viewPassword: true, + collectionIds: [], + revisionDate: "2023-08-03T17:40:59.793Z", + creationDate: "2023-08-03T17:40:59.793Z", + deletedDate: null, + reprompt: 0, + localData: null, + }, + { + initializerKey: 1, + id: "cbea34a8-bde4-46ad-9d19-b05001227nm7", + organizationId: null, + folderId: null, + name: "Can Be Edited id ending 7", + notes: null, + type: 1, + favorite: false, + organizationUseTotp: false, + login: { + password: "123", + }, + edit: true, + viewPassword: true, + collectionIds: [], + revisionDate: "2023-08-03T17:40:59.793Z", + creationDate: "2023-08-03T17:40:59.793Z", + deletedDate: null, + reprompt: 0, + localData: null, + }, ]; diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 9d16bbb1c62..42e81b0d44b 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -74,7 +74,7 @@ describe("ReusedPasswordsReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(2); + expect(component.ciphers.length).toEqual(3); expect(component.ciphers[0].id).toEqual(expectedIdOne); expect(component.ciphers[0].edit).toEqual(true); expect(component.ciphers[1].id).toEqual(expectedIdTwo); diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index bcace60ac0c..177094b3750 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -86,7 +86,7 @@ describe("WeakPasswordsReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(2); + expect(component.ciphers.length).toEqual(4); expect(component.ciphers[0].id).toEqual(expectedIdOne); expect(component.ciphers[0].edit).toEqual(true); expect(component.ciphers[1].id).toEqual(expectedIdTwo); From 92518288d38945aa6b07b7233a857c6b28f0ff0c Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Fri, 11 Oct 2024 17:49:44 -0700 Subject: [PATCH 2/9] fix tests --- .../password-health.component.spec.ts | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts index 15e3c705914..98893a9f0c6 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -10,24 +10,21 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { - TableBodyDirective, - TableComponent, -} from "@bitwarden/components/src/table/table.component"; +import { TableModule } from "@bitwarden/components"; +import { TableBodyDirective } from "@bitwarden/components/src/table/table.component"; import { PasswordRepromptService } from "@bitwarden/vault"; // eslint-disable-next-line no-restricted-imports import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module"; -// eslint-disable-next-line no-restricted-imports -import { PasswordsReportComponent } from "../reports/pages/passwords-report.component"; -// eslint-disable-next-line no-restricted-imports -import { userData } from "../reports/pages/passwords-report.mock"; // eslint-disable-next-line no-restricted-imports import { cipherData } from "../reports/pages/reports-ciphers.mock"; -describe("PasswordsReportComponent", () => { - let component: PasswordsReportComponent; - let fixture: ComponentFixture; +import { PasswordHealthComponent } from "./password-health.component"; +import { userData } from "./password-health.mock"; + +describe("PasswordHealthComponent", () => { + let component: PasswordHealthComponent; + let fixture: ComponentFixture; let passwordStrengthService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; @@ -44,8 +41,8 @@ describe("PasswordsReportComponent", () => { organizationService.organizations$ = of([]); await TestBed.configureTestingModule({ - imports: [PipesModule], - declarations: [PasswordsReportComponent, I18nPipe, TableComponent, TableBodyDirective], + imports: [PipesModule, TableModule, TableBodyDirective], + declarations: [I18nPipe], providers: [ { provide: CipherService, useValue: cipherServiceMock }, { provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService }, @@ -60,7 +57,7 @@ describe("PasswordsReportComponent", () => { }); beforeEach(() => { - fixture = TestBed.createComponent(PasswordsReportComponent); + fixture = TestBed.createComponent(PasswordHealthComponent); component = fixture.componentInstance; (component as any).cipherData = cipherData; From 17212555b3d4cea1a6a6622d9e343e5316930e64 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Mon, 14 Oct 2024 15:09:20 -0700 Subject: [PATCH 3/9] simplify logic. fix tests --- .../password-health.component.html | 35 +-- .../password-health.component.spec.ts | 13 +- .../password-health.component.ts | 218 +++++++----------- .../services/config/default-config.service.ts | 2 + 4 files changed, 97 insertions(+), 171 deletions(-) diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.html b/apps/web/src/app/tools/access-intelligence/password-health.component.html index c6d424f0433..9274155ace6 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.html +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.html @@ -14,7 +14,6 @@ {{ "name" | i18n }} - {{ "organization" | i18n }} {{ "weakness" | i18n }} {{ "timesReused" | i18n }} {{ "timesExposed" | i18n }} @@ -26,44 +25,12 @@ - - {{ - r.name - }} - - + {{ r.name }} - - - - {{ "shared" | i18n }} - - - - {{ "attachments" | i18n }}
{{ r.subTitle }} - - - - { auditServiceMock = mock(); organizationService = mock(); syncServiceMock = mock(); - cipherServiceMock = mock(); + cipherServiceMock = mock({ + getAllDecrypted: jest.fn().mockResolvedValue(cipherData), + }); organizationService.organizations$ = of([]); await TestBed.configureTestingModule({ - imports: [PipesModule, TableModule, TableBodyDirective], - declarations: [I18nPipe], + imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule], + declarations: [TableBodyDirective], providers: [ { provide: CipherService, useValue: cipherServiceMock }, { provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService }, diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.ts index 5b0f55fd3a4..3d65b27a598 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.ts +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.ts @@ -42,13 +42,10 @@ import { CipherReportComponent } from "../reports/pages/cipher-report.component" export class PasswordHealthComponent extends CipherReportComponent implements OnInit { passwordStrengthMap = new Map(); - private passwordStrengthCache = new Map(); weakPasswordCiphers: CipherView[] = []; - reusedPasswordCiphers: CipherView[] = []; - passwordUseMap: Map; + passwordUseMap = new Map(); - exposedPasswordCiphers: CipherView[] = []; exposedPasswordMap = new Map(); reportCiphers: CipherView[] = []; @@ -80,10 +77,12 @@ export class PasswordHealthComponent extends CipherReportComponent implements On async setCiphers() { const allCiphers = await this.getAllCiphers(); - this.filterStatus = [0]; - this.setWeakPasswordMap(allCiphers); - this.setReusedPasswordMap(allCiphers); - await this.setExposedPasswordMap(allCiphers); + allCiphers.forEach(async (cipher) => { + this.findWeakPassword(cipher); + this.findReusedPassword(cipher); + await this.findExposedPassword(cipher); + }); + this.filterCiphersByOrg(this.reportCiphers); // const reportIssues = allCiphers.map((c) => { // if (this.passwordStrengthMap.has(c.id)) { @@ -98,151 +97,108 @@ export class PasswordHealthComponent extends CipherReportComponent implements On // return c; // } // }); - - this.filterCiphersByOrg(this.reportCiphers); } - protected setWeakPasswordMap(ciphers: any[]) { - this.weakPasswordCiphers = []; - this.filterStatus = [0]; - this.findWeakPasswords(ciphers); + protected checkForExistingCipher(ciph: CipherView) { + if (!this.reportCipherIds.includes(ciph.id)) { + this.reportCipherIds.push(ciph.id); + this.reportCiphers.push(ciph); + } } - protected async setExposedPasswordMap(ciphers: any[]) { - const promises: Promise[] = []; - - ciphers.forEach((ciph: any) => { - const { type, login, isDeleted, edit, viewPassword, id } = ciph; - if ( - type !== CipherType.Login || - login.password == null || - login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword - ) { - return; - } + protected async findExposedPassword(cipher: CipherView) { + const { type, login, isDeleted, edit, viewPassword, id } = cipher; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } - const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => { - if (exposedCount > 0) { - this.exposedPasswordCiphers.push(ciph); - this.exposedPasswordMap.set(id, exposedCount); - if (!this.reportCipherIds.includes(ciph.id)) { - this.reportCipherIds.push(ciph.id); - this.reportCiphers.push(ciph); - } - } - }); - promises.push(promise); - }); - await Promise.all(promises); + const exposedCount = await this.auditService.passwordLeaked(login.password); + if (exposedCount > 0) { + this.exposedPasswordMap.set(id, exposedCount); + this.checkForExistingCipher(cipher); + } } - protected setReusedPasswordMap(ciphers: any[]): void { - const ciphersWithPasswords: CipherView[] = []; - this.passwordUseMap = new Map(); + protected findReusedPassword(cipher: CipherView) { + const { type, login, isDeleted, edit, viewPassword } = cipher; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } - ciphers.forEach((ciph) => { - const { type, login, isDeleted, edit, viewPassword } = ciph; - if ( - type !== CipherType.Login || - login.password == null || - login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword - ) { - return; - } + if (this.passwordUseMap.has(login.password)) { + this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1); + } else { + this.passwordUseMap.set(login.password, 1); + } - ciphersWithPasswords.push(ciph); - if (this.passwordUseMap.has(login.password)) { - this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) || 0 + 1); - } else { - this.passwordUseMap.set(login.password, 1); - } - }); - this.reusedPasswordCiphers = ciphersWithPasswords.filter( - (c) => - (this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password)) || - 0 > 1, - ); + this.checkForExistingCipher(cipher); } - protected findWeakPasswords(ciphers: any[]): void { - ciphers.forEach((ciph) => { - const { type, login, isDeleted, edit, viewPassword, id } = ciph; - if ( - type !== CipherType.Login || - login.password == null || - login.password === "" || - isDeleted || - (!this.organization && !edit) || - !viewPassword - ) { - return; - } + protected findWeakPassword(cipher: CipherView): void { + const { type, login, isDeleted, edit, viewPassword } = cipher; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + (!this.organization && !edit) || + !viewPassword + ) { + return; + } - 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 + const hasUserName = this.isUserNameNotEmpty(cipher); + let userInput: string[] = []; + if (hasUserName) { + const atPosition = login.username.indexOf("@"); + if (atPosition > -1) { + userInput = userInput + .concat( + login.username + .substring(0, atPosition) .trim() .toLowerCase() - .split(/[^A-Za-z0-9]/) - .filter((i: any) => 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); + .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); } - }); - this.weakPasswordCiphers.sort((a, b) => { - return ( - (this.passwordStrengthCache.get(this.getCacheKey(a)) || 0) - - (this.passwordStrengthCache.get(this.getCacheKey(b)) || 0) - ); - }); - } + } + const { score } = this.passwordStrengthService.getPasswordStrength( + login.password, + null, + userInput.length > 0 ? userInput : null, + ); - protected canManageCipher(c: CipherView): boolean { - // this will only ever be false from the org view; - return true; + if (score != null && score <= 2) { + this.passwordStrengthMap.set(cipher.id, this.scoreKey(score)); + this.checkForExistingCipher(cipher); + } } private isUserNameNotEmpty(c: CipherView): boolean { 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] { switch (score) { case 4: diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index e0603ed509b..e14da148691 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -127,6 +127,8 @@ export class DefaultConfigService implements ConfigService { return DefaultFeatureFlagValue[flag]; } + serverConfig.featureStates[FeatureFlag.AccessIntelligence] = true; + return serverConfig.featureStates[flag] as FeatureFlagValueType; } From 14dade1de5f066fdccab79ec3c02c9228b30dc75 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Mon, 14 Oct 2024 15:10:40 -0700 Subject: [PATCH 4/9] revert change to default config service --- .../src/platform/services/config/default-config.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index e14da148691..e0603ed509b 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -127,8 +127,6 @@ export class DefaultConfigService implements ConfigService { return DefaultFeatureFlagValue[flag]; } - serverConfig.featureStates[FeatureFlag.AccessIntelligence] = true; - return serverConfig.featureStates[flag] as FeatureFlagValueType; } From 41bfbd47fdf525a724768eab9b5117914d750e75 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Tue, 15 Oct 2024 10:13:58 -0700 Subject: [PATCH 5/9] remove cipher report dep. fix tests. --- .../password-health.component.html | 4 +- .../password-health.component.spec.ts | 25 ++-------- .../password-health.component.ts | 46 ++++++++++--------- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.html b/apps/web/src/app/tools/access-intelligence/password-health.component.html index 9274155ace6..32459706449 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.html +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.html @@ -1,6 +1,6 @@

{{ "passwordsReportDesc" | i18n }}

-
+
{{ "loading" | i18n }}
-
+
diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts index 2e304892033..d21d8d75f45 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -2,16 +2,14 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TableModule } from "@bitwarden/components"; import { TableBodyDirective } from "@bitwarden/components/src/table/table.component"; -import { PasswordRepromptService } from "@bitwarden/vault"; import { LooseComponentsModule } from "../../shared"; import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; @@ -19,14 +17,12 @@ import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; import { cipherData } from "../reports/pages/reports-ciphers.mock"; import { PasswordHealthComponent } from "./password-health.component"; -import { userData } from "./password-health.mock"; describe("PasswordHealthComponent", () => { let component: PasswordHealthComponent; let fixture: ComponentFixture; let passwordStrengthService: MockProxy; let organizationService: MockProxy; - let syncServiceMock: MockProxy; let cipherServiceMock: MockProxy; let auditServiceMock: MockProxy; @@ -34,25 +30,20 @@ describe("PasswordHealthComponent", () => { passwordStrengthService = mock(); auditServiceMock = mock(); organizationService = mock(); - syncServiceMock = mock(); + organizationService.organizations$ = of([{ id: "orgId" } as Organization]); cipherServiceMock = mock({ - getAllDecrypted: jest.fn().mockResolvedValue(cipherData), + getAllFromApiForOrganization: jest.fn().mockResolvedValue(cipherData), }); - organizationService.organizations$ = of([]); - await TestBed.configureTestingModule({ imports: [PasswordHealthComponent, PipesModule, TableModule, LooseComponentsModule], declarations: [TableBodyDirective], providers: [ { provide: CipherService, useValue: cipherServiceMock }, { provide: PasswordStrengthServiceAbstraction, useValue: passwordStrengthService }, - { provide: AuditService, useValue: auditServiceMock }, { provide: OrganizationService, useValue: organizationService }, - { provide: ModalService, useValue: mock() }, - { provide: PasswordRepromptService, useValue: mock() }, - { provide: SyncService, useValue: syncServiceMock }, { provide: I18nService, useValue: mock() }, + { provide: AuditService, useValue: auditServiceMock }, ], }).compileComponents(); }); @@ -61,9 +52,6 @@ describe("PasswordHealthComponent", () => { fixture = TestBed.createComponent(PasswordHealthComponent); component = fixture.componentInstance; - (component as any).cipherData = cipherData; - (component as any).userData = userData; - fixture.detectChanges(); }); @@ -81,6 +69,7 @@ describe("PasswordHealthComponent", () => { const cipherIds = component.reportCiphers.map((c) => c.id); expect(cipherIds).toEqual([ + "cbea34a8-bde4-46ad-9d19-b05001228ab1", "cbea34a8-bde4-46ad-9d19-b05001228ab2", "cbea34a8-bde4-46ad-9d19-b05001228cd3", "cbea34a8-bde4-46ad-9d19-b05001227nm6", @@ -89,10 +78,6 @@ describe("PasswordHealthComponent", () => { expect(component.reportCiphers.length).toEqual(4); }); - it("should call fullSync method of syncService", () => { - expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); - }); - it("should correctly populate passwordStrengthMap", async () => { passwordStrengthService.getPasswordStrength.mockImplementation((password) => { let score = 0; diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.ts index 3d65b27a598..4a3264aa399 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.ts +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.ts @@ -1,19 +1,24 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { BadgeModule, BadgeVariant, ContainerComponent, TableModule } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { + BadgeModule, + BadgeVariant, + ContainerComponent, + TableDataSource, + TableModule, +} from "@bitwarden/components"; // eslint-disable-next-line no-restricted-imports import { HeaderModule } from "../../layouts/header/header.module"; @@ -21,8 +26,6 @@ import { HeaderModule } from "../../layouts/header/header.module"; import { OrganizationBadgeModule } from "../../vault/individual-vault/organization-badge/organization-badge.module"; // eslint-disable-next-line no-restricted-imports import { PipesModule } from "../../vault/individual-vault/pipes/pipes.module"; -// eslint-disable-next-line no-restricted-imports -import { CipherReportComponent } from "../reports/pages/cipher-report.component"; @Component({ standalone: true, @@ -39,7 +42,7 @@ import { CipherReportComponent } from "../reports/pages/cipher-report.component" TableModule, ], }) -export class PasswordHealthComponent extends CipherReportComponent implements OnInit { +export class PasswordHealthComponent implements OnInit { passwordStrengthMap = new Map(); weakPasswordCiphers: CipherView[] = []; @@ -48,41 +51,40 @@ export class PasswordHealthComponent extends CipherReportComponent implements On exposedPasswordMap = new Map(); + dataSource = new TableDataSource(); + reportCiphers: CipherView[] = []; reportCipherIds: string[] = []; + organization: Organization; + + loading = true; + constructor( protected cipherService: CipherService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected organizationService: OrganizationService, protected auditService: AuditService, - modalService: ModalService, - passwordRepromptService: PasswordRepromptService, - i18nService: I18nService, - syncService: SyncService, + protected i18nService: I18nService, ) { - super( - cipherService, - modalService, - passwordRepromptService, - organizationService, - i18nService, - syncService, - ); + this.organizationService.organizations$.pipe(takeUntilDestroyed()).subscribe((orgs) => { + this.organization = orgs[0]; + }); } async ngOnInit() { - await super.load(); + await this.setCiphers(); } async setCiphers() { - const allCiphers = await this.getAllCiphers(); + const allCiphers = await this.cipherService.getAllFromApiForOrganization(this.organization.id); allCiphers.forEach(async (cipher) => { this.findWeakPassword(cipher); this.findReusedPassword(cipher); await this.findExposedPassword(cipher); }); - this.filterCiphersByOrg(this.reportCiphers); + this.dataSource.data = this.reportCiphers; + this.loading = false; // const reportIssues = allCiphers.map((c) => { // if (this.passwordStrengthMap.has(c.id)) { From 67ef309c85358714549931735fe8ec3b7cd712e4 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Tue, 15 Oct 2024 10:17:52 -0700 Subject: [PATCH 6/9] revert changes to mock data and specs --- .../password-health.component.spec.ts | 4 +- ...exposed-passwords-report.component.spec.ts | 2 +- .../reports/pages/reports-ciphers.mock.ts | 44 ------------------- .../reused-passwords-report.component.spec.ts | 2 +- .../weak-passwords-report.component.spec.ts | 2 +- 5 files changed, 4 insertions(+), 50 deletions(-) diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts index d21d8d75f45..a1f3c9b7a50 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -72,10 +72,8 @@ describe("PasswordHealthComponent", () => { "cbea34a8-bde4-46ad-9d19-b05001228ab1", "cbea34a8-bde4-46ad-9d19-b05001228ab2", "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001227nm6", - "cbea34a8-bde4-46ad-9d19-b05001227nm7", ]); - expect(component.reportCiphers.length).toEqual(4); + expect(component.reportCiphers.length).toEqual(3); }); it("should correctly populate passwordStrengthMap", async () => { diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index ea2f7c7d516..07dc218bd64 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -83,7 +83,7 @@ describe("ExposedPasswordsReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(4); + expect(component.ciphers.length).toEqual(2); expect(component.ciphers[0].id).toEqual(expectedIdOne); expect(component.ciphers[0].edit).toEqual(true); expect(component.ciphers[1].id).toEqual(expectedIdTwo); diff --git a/apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts b/apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts index 0b594165718..195c9afa9d2 100644 --- a/apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts +++ b/apps/web/src/app/tools/reports/pages/reports-ciphers.mock.ts @@ -125,48 +125,4 @@ export const cipherData: any[] = [ reprompt: 0, localData: null, }, - { - initializerKey: 1, - id: "cbea34a8-bde4-46ad-9d19-b05001227nm6", - organizationId: null, - folderId: null, - name: "Can Be Edited id ending 6", - notes: null, - type: 1, - favorite: false, - organizationUseTotp: false, - login: { - password: "TestPass123", - }, - edit: true, - viewPassword: true, - collectionIds: [], - revisionDate: "2023-08-03T17:40:59.793Z", - creationDate: "2023-08-03T17:40:59.793Z", - deletedDate: null, - reprompt: 0, - localData: null, - }, - { - initializerKey: 1, - id: "cbea34a8-bde4-46ad-9d19-b05001227nm7", - organizationId: null, - folderId: null, - name: "Can Be Edited id ending 7", - notes: null, - type: 1, - favorite: false, - organizationUseTotp: false, - login: { - password: "123", - }, - edit: true, - viewPassword: true, - collectionIds: [], - revisionDate: "2023-08-03T17:40:59.793Z", - creationDate: "2023-08-03T17:40:59.793Z", - deletedDate: null, - reprompt: 0, - localData: null, - }, ]; diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 42e81b0d44b..9d16bbb1c62 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -74,7 +74,7 @@ describe("ReusedPasswordsReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(3); + expect(component.ciphers.length).toEqual(2); expect(component.ciphers[0].id).toEqual(expectedIdOne); expect(component.ciphers[0].edit).toEqual(true); expect(component.ciphers[1].id).toEqual(expectedIdTwo); diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index 177094b3750..bcace60ac0c 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -86,7 +86,7 @@ describe("WeakPasswordsReportComponent", () => { jest.spyOn(component as any, "getAllCiphers").mockReturnValue(Promise.resolve(cipherData)); await component.setCiphers(); - expect(component.ciphers.length).toEqual(4); + expect(component.ciphers.length).toEqual(2); expect(component.ciphers[0].id).toEqual(expectedIdOne); expect(component.ciphers[0].edit).toEqual(true); expect(component.ciphers[1].id).toEqual(expectedIdTwo); From ba96d2b7cc90721fd093829e2b7d210a7bb8f033 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Tue, 15 Oct 2024 10:19:39 -0700 Subject: [PATCH 7/9] remove mock data --- .../password-health.mock.ts | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 apps/web/src/app/tools/access-intelligence/password-health.mock.ts diff --git a/apps/web/src/app/tools/access-intelligence/password-health.mock.ts b/apps/web/src/app/tools/access-intelligence/password-health.mock.ts deleted file mode 100644 index d01edc37a59..00000000000 --- a/apps/web/src/app/tools/access-intelligence/password-health.mock.ts +++ /dev/null @@ -1,66 +0,0 @@ -export const userData: any[] = [ - { - userName: "David Brent", - email: "david.brent@wernhamhogg.uk", - usesKeyConnector: true, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab1", - "cbea34a8-bde4-46ad-9d19-b05001228ab2", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - "cbea34a8-bde4-46ad-9d19-b05001227nm5", - ], - }, - { - userName: "Tim Canterbury", - email: "tim.canterbury@wernhamhogg.uk", - usesKeyConnector: false, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab2", - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - "cbea34a8-bde4-46ad-9d19-b05001227nm5", - ], - }, - { - userName: "Gareth Keenan", - email: "gareth.keenan@wernhamhogg.uk", - usesKeyConnector: true, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - "cbea34a8-bde4-46ad-9d19-b05001227nm5", - "cbea34a8-bde4-46ad-9d19-b05001227nm7", - ], - }, - { - userName: "Dawn Tinsley", - email: "dawn.tinsley@wernhamhogg.uk", - usesKeyConnector: true, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab2", - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - ], - }, - { - userName: "Keith Bishop", - email: "keith.bishop@wernhamhogg.uk", - usesKeyConnector: false, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab1", - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - "cbea34a8-bde4-46ad-9d19-b05001227nm5", - ], - }, - { - userName: "Chris Finch", - email: "chris.finch@wernhamhogg.uk", - usesKeyConnector: true, - cipherIds: [ - "cbea34a8-bde4-46ad-9d19-b05001228ab2", - "cbea34a8-bde4-46ad-9d19-b05001228cd3", - "cbea34a8-bde4-46ad-9d19-b05001228xy4", - ], - }, -]; From 7c05470bd35dbf90a00fc8ec71de221d79d17026 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Tue, 15 Oct 2024 12:27:09 -0700 Subject: [PATCH 8/9] use orgId param --- .../password-health.component.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.ts index 4a3264aa399..6e8e62c50db 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.ts +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.ts @@ -1,6 +1,8 @@ import { CommonModule } from "@angular/common"; -import { Component, OnInit } from "@angular/core"; +import { Component, DestroyRef, inject, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute } from "@angular/router"; +import { from, map, switchMap, tap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -60,20 +62,31 @@ export class PasswordHealthComponent implements OnInit { loading = true; + private destroyRef = inject(DestroyRef); + constructor( protected cipherService: CipherService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected organizationService: OrganizationService, protected auditService: AuditService, protected i18nService: I18nService, - ) { - this.organizationService.organizations$.pipe(takeUntilDestroyed()).subscribe((orgs) => { - this.organization = orgs[0]; - }); - } - - async ngOnInit() { - await this.setCiphers(); + protected activatedRoute: ActivatedRoute, + ) {} + + ngOnInit() { + this.activatedRoute.paramMap + .pipe( + takeUntilDestroyed(this.destroyRef), + map((params) => params.get("organizationId")), + switchMap((organizationId) => { + return from(this.organizationService.get(organizationId)); + }), + tap((organization) => { + this.organization = organization; + }), + switchMap(() => from(this.setCiphers())), + ) + .subscribe(); } async setCiphers() { From 2ecf2357962772a555145d39a4bfd0b49b26a2f4 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Tue, 15 Oct 2024 13:35:04 -0700 Subject: [PATCH 9/9] fix test --- .../password-health.component.spec.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts index a1f3c9b7a50..4a6d5c50ee1 100644 --- a/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts +++ b/apps/web/src/app/tools/access-intelligence/password-health.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ActivatedRoute, convertToParamMap } from "@angular/router"; import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; @@ -25,12 +26,14 @@ describe("PasswordHealthComponent", () => { let organizationService: MockProxy; let cipherServiceMock: MockProxy; let auditServiceMock: MockProxy; + const activeRouteParams = convertToParamMap({ organizationId: "orgId" }); beforeEach(async () => { passwordStrengthService = mock(); auditServiceMock = mock(); - organizationService = mock(); - organizationService.organizations$ = of([{ id: "orgId" } as Organization]); + organizationService = mock({ + get: jest.fn().mockResolvedValue({ id: "orgId" } as Organization), + }); cipherServiceMock = mock({ getAllFromApiForOrganization: jest.fn().mockResolvedValue(cipherData), }); @@ -44,6 +47,13 @@ describe("PasswordHealthComponent", () => { { provide: OrganizationService, useValue: organizationService }, { provide: I18nService, useValue: mock() }, { provide: AuditService, useValue: auditServiceMock }, + { + provide: ActivatedRoute, + useValue: { + paramMap: of(activeRouteParams), + url: of([]), + }, + }, ], }).compileComponents(); });