From 769d67af39ee96436a37690802354cb341accbb2 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:32:26 -0500 Subject: [PATCH 01/12] [SM-1292] Add secret view dialog (#9445) * Add secret view dialog * Use secret view dialog --- apps/web/src/locales/en/messages.json | 3 ++ .../overview/overview.component.html | 1 + .../overview/overview.component.ts | 13 +++++++ .../project/project-secrets.component.html | 1 + .../project/project-secrets.component.ts | 13 +++++++ .../dialog/secret-view-dialog.component.html | 30 ++++++++++++++ .../dialog/secret-view-dialog.component.ts | 39 +++++++++++++++++++ .../secrets/secrets.component.html | 1 + .../secrets/secrets.component.ts | 13 +++++++ .../secrets-manager/secrets/secrets.module.ts | 8 +++- .../shared/secrets-list.component.html | 4 +- .../shared/secrets-list.component.ts | 9 +++++ 12 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.html create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index d7a21ad6d6a..61c92ba407a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -8354,5 +8354,8 @@ }, "verified": { "message": "Verified" + }, + "viewSecret": { + "message": "View secret" } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html index 255877e4e8d..29cbf78464b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.html @@ -65,6 +65,7 @@ (deleteSecretsEvent)="openDeleteSecret($event)" (newSecretEvent)="openNewSecretDialog()" (editSecretEvent)="openEditSecret($event)" + (viewSecretEvent)="openViewSecret($event)" (copySecretNameEvent)="copySecretName($event)" (copySecretValueEvent)="copySecretValue($event)" (copySecretUuidEvent)="copySecretUuid($event)" diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts index 56c02e1ed43..4c057e56988 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/overview/overview.component.ts @@ -41,6 +41,10 @@ import { SecretDialogComponent, SecretOperation, } from "../secrets/dialog/secret-dialog.component"; +import { + SecretViewDialogComponent, + SecretViewDialogParams, +} from "../secrets/dialog/secret-view-dialog.component"; import { SecretService } from "../secrets/secret.service"; import { ServiceAccountDialogComponent, @@ -277,6 +281,15 @@ export class OverviewComponent implements OnInit, OnDestroy { }); } + openViewSecret(secretId: string) { + this.dialogService.open(SecretViewDialogComponent, { + data: { + organizationId: this.organizationId, + secretId: secretId, + }, + }); + } + openDeleteSecret(event: SecretListView[]) { this.dialogService.open(SecretDeleteDialogComponent, { data: { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html index 980f38ca157..1ab8b7e0196 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.html @@ -14,6 +14,7 @@ (deleteSecretsEvent)="openDeleteSecret($event)" (newSecretEvent)="openNewSecretDialog()" (editSecretEvent)="openEditSecret($event)" + (viewSecretEvent)="openViewSecret($event)" (copySecretNameEvent)="copySecretName($event)" (copySecretValueEvent)="copySecretValue($event)" (copySecretUuidEvent)="copySecretUuid($event)" diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts index 21d6e576a01..b766de1ebd0 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-secrets.component.ts @@ -19,6 +19,10 @@ import { SecretDialogComponent, SecretOperation, } from "../../secrets/dialog/secret-dialog.component"; +import { + SecretViewDialogComponent, + SecretViewDialogParams, +} from "../../secrets/dialog/secret-view-dialog.component"; import { SecretService } from "../../secrets/secret.service"; import { SecretsListComponent } from "../../shared/secrets-list.component"; import { ProjectService } from "../project.service"; @@ -88,6 +92,15 @@ export class ProjectSecretsComponent { }); } + openViewSecret(secretId: string) { + this.dialogService.open(SecretViewDialogComponent, { + data: { + organizationId: this.organizationId, + secretId: secretId, + }, + }); + } + openDeleteSecret(event: SecretListView[]) { this.dialogService.open(SecretDeleteDialogComponent, { data: { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.html new file mode 100644 index 00000000000..60fa1f268c2 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.html @@ -0,0 +1,30 @@ +
+ + +
+ + {{ "name" | i18n }} + + + + {{ "value" | i18n }} + + +
+ + {{ "notes" | i18n }} + + +
+ + + +
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts new file mode 100644 index 00000000000..a113fd2ffa4 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/dialog/secret-view-dialog.component.ts @@ -0,0 +1,39 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; + +import { SecretService } from "../secret.service"; + +export interface SecretViewDialogParams { + organizationId: string; + secretId: string; +} + +@Component({ + templateUrl: "./secret-view-dialog.component.html", +}) +export class SecretViewDialogComponent implements OnInit { + protected loading = true; + protected formGroup = new FormGroup({ + name: new FormControl(""), + value: new FormControl(""), + notes: new FormControl(""), + }); + + constructor( + private secretService: SecretService, + @Inject(DIALOG_DATA) private params: SecretViewDialogParams, + ) {} + + async ngOnInit() { + this.loading = true; + const secret = await this.secretService.getBySecretId(this.params.secretId); + this.formGroup.setValue({ + name: secret.name, + value: secret.value, + notes: secret.note, + }); + this.formGroup.disable(); + this.loading = false; + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.html index 13595d97205..b12f5c9f184 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.html @@ -10,6 +10,7 @@ (deleteSecretsEvent)="openDeleteSecret($event)" (newSecretEvent)="openNewSecretDialog()" (editSecretEvent)="openEditSecret($event)" + (viewSecretEvent)="openViewSecret($event)" (copySecretNameEvent)="copySecretName($event)" (copySecretValueEvent)="copySecretValue($event)" (copySecretUuidEvent)="copySecretUuid($event)" diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts index 2717f96a686..1744e970361 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.component.ts @@ -20,6 +20,10 @@ import { SecretDialogComponent, SecretOperation, } from "./dialog/secret-dialog.component"; +import { + SecretViewDialogComponent, + SecretViewDialogParams, +} from "./dialog/secret-view-dialog.component"; import { SecretService } from "./secret.service"; @Component({ @@ -77,6 +81,15 @@ export class SecretsComponent implements OnInit { }); } + openViewSecret(secretId: string) { + this.dialogService.open(SecretViewDialogComponent, { + data: { + organizationId: this.organizationId, + secretId: secretId, + }, + }); + } + openDeleteSecret(event: SecretListView[]) { this.dialogService.open(SecretDeleteDialogComponent, { data: { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.module.ts index 356021817b4..2ae5bfa3b8e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/secrets/secrets.module.ts @@ -4,12 +4,18 @@ import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; import { SecretDeleteDialogComponent } from "./dialog/secret-delete.component"; import { SecretDialogComponent } from "./dialog/secret-dialog.component"; +import { SecretViewDialogComponent } from "./dialog/secret-view-dialog.component"; import { SecretsRoutingModule } from "./secrets-routing.module"; import { SecretsComponent } from "./secrets.component"; @NgModule({ imports: [SecretsManagerSharedModule, SecretsRoutingModule], - declarations: [SecretDeleteDialogComponent, SecretDialogComponent, SecretsComponent], + declarations: [ + SecretDeleteDialogComponent, + SecretDialogComponent, + SecretViewDialogComponent, + SecretsComponent, + ], providers: [], }) export class SecretsModule {} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html index 4b629ca4885..859c7417eb8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/secrets-list.component.html @@ -66,7 +66,7 @@
-
@@ -118,7 +118,7 @@ - -
- - - +
+

{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}

+ + +
diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 77e51172269..1115a60bf91 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -176,12 +176,6 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { titleId: "updatePassword" } satisfies DataProperties, }, - { - path: "remove-password", - component: RemovePasswordComponent, - canActivate: [AuthGuard], - data: { titleId: "removeMasterPassword" } satisfies DataProperties, - }, { path: "migrate-legacy-encryption", loadComponent: () => @@ -195,25 +189,6 @@ const routes: Routes = [ path: "", component: AnonLayoutWrapperComponent, children: [ - { - path: "recover-2fa", - canActivate: [unauthGuardFn()], - children: [ - { - path: "", - component: RecoverTwoFactorComponent, - }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - data: { - pageTitle: "recoverAccountTwoStep", - titleId: "recoverAccountTwoStep", - } satisfies DataProperties & AnonLayoutWrapperData, - }, { path: "accept-emergency", canActivate: [deepLinkGuard()], @@ -237,6 +212,34 @@ const routes: Routes = [ }, ], }, + { + path: "recover-2fa", + canActivate: [unauthGuardFn()], + children: [ + { + path: "", + component: RecoverTwoFactorComponent, + }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + data: { + pageTitle: "recoverAccountTwoStep", + titleId: "recoverAccountTwoStep", + } satisfies DataProperties & AnonLayoutWrapperData, + }, + { + path: "remove-password", + component: RemovePasswordComponent, + canActivate: [AuthGuard], + data: { + pageTitle: "removeMasterPassword", + titleId: "removeMasterPassword", + } satisfies DataProperties & AnonLayoutWrapperData, + }, ], }, { From 700acc069b0b8d817533f9b57c90a6c947c5260b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:24:14 -0400 Subject: [PATCH 05/12] [deps] Autofill: Update tldts to v6.1.25 (#9559) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- apps/cli/package.json | 2 +- package-lock.json | 20 +++++++++++--------- package.json | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 1ad09cc17a5..e992c3b6725 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -80,7 +80,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.22", + "tldts": "6.1.25", "zxcvbn": "4.4.2" } } diff --git a/package-lock.json b/package-lock.json index c5db91b8482..fec3db2aea0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.22", + "tldts": "6.1.25", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -228,7 +228,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.22", + "tldts": "6.1.25", "zxcvbn": "4.4.2" }, "bin": { @@ -37431,20 +37431,22 @@ "dev": true }, "node_modules/tldts": { - "version": "6.1.22", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.22.tgz", - "integrity": "sha512-WwWAPp+zJn8oJkpjqJcSuuj5foL9cI8SiTjH+gGS1bw5N163YywM0Cmd9OijwtKjdGG7OC6NEYZVl4EG8HfSMg==", + "version": "6.1.25", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.25.tgz", + "integrity": "sha512-UmjB1dVArio9hny1D84VFeEvE37nCyfW5sWHr7AUV2MxJgxD8NR/kdmEMyjx5o/kRuOOBbaaXStce2R5C6I1Gg==", + "license": "MIT", "dependencies": { - "tldts-core": "^6.1.22" + "tldts-core": "^6.1.25" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.22", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.22.tgz", - "integrity": "sha512-TMCyBC7HpvDpBRQCLsODmsclNXGhZLSj76gIlx7QcwvKElMdIzhGN5iYcuTI7yAWJm8zTpsVehWCeOGytDY9fg==" + "version": "6.1.25", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.25.tgz", + "integrity": "sha512-hbSsjJOeDMV91JiqcrrFQ46D7EepH880zVmPjnBDmt3P+h0Aowz8Nh1adIcqkdhJbgpzZYQr6aM8/N3tZC6JyA==", + "license": "MIT" }, "node_modules/tmp": { "version": "0.0.33", diff --git a/package.json b/package.json index 9670da6f676..93a6b512803 100644 --- a/package.json +++ b/package.json @@ -204,7 +204,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.22", + "tldts": "6.1.25", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" From 7fb94082024f15ad5cd1c52be633663f3f1924ee Mon Sep 17 00:00:00 2001 From: Dillon Beresford <165616268+bwdil@users.noreply.github.com> Date: Mon, 10 Jun 2024 10:54:24 -0500 Subject: [PATCH 06/12] [PM-7025] include check-run in workflows where secrets are used (#9135) * include check-run in workflows where secrets are used * revert changes in build-cli workflow and add check-run to codecov * assert token permissions --------- Co-authored-by: Matt Bishop --- .github/workflows/test.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12649b91ea9..cb4a18947be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,16 +8,26 @@ on: - "main" - "rc" - "hotfix-rc-*" - pull_request: + pull_request_target: + types: [opened, synchronize] defaults: run: shell: bash jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + test: name: Run tests runs-on: ubuntu-22.04 + needs: check-run + permissions: + contents: read + pull-requests: write + steps: - name: Checkout repo uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 From 19f2d2aefc3cd411511638f6c80c4695580b030e Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 10 Jun 2024 09:55:12 -0700 Subject: [PATCH 07/12] [PM-8379] Update vault popup items service to track loading state (#9528) --- .../vault-popup-items.service.spec.ts | 48 +++++++++++++++++++ .../services/vault-popup-items.service.ts | 35 ++++++++++---- libs/common/spec/observable-tracker.ts | 9 ++-- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index b7091eb87bf..f08f4e836e1 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -379,6 +379,54 @@ describe("VaultPopupItemsService", () => { }); }); + describe("loading$", () => { + let tracked: ObservableTracker; + let trackedCiphers: ObservableTracker; + beforeEach(() => { + // Start tracking loading$ emissions + tracked = new ObservableTracker(service.loading$); + + // Track remainingCiphers$ to make cipher observables active + trackedCiphers = new ObservableTracker(service.remainingCiphers$); + }); + + it("should initialize with true first", async () => { + expect(tracked.emissions[0]).toBe(true); + }); + + it("should emit false once ciphers are available", async () => { + expect(tracked.emissions.length).toBe(2); + expect(tracked.emissions[0]).toBe(true); + expect(tracked.emissions[1]).toBe(false); + }); + + it("should cycle when cipherService.ciphers$ emits", async () => { + // Restart tracking + tracked = new ObservableTracker(service.loading$); + (cipherServiceMock.ciphers$ as BehaviorSubject).next(null); + + await trackedCiphers.pauseUntilReceived(2); + + expect(tracked.emissions.length).toBe(3); + expect(tracked.emissions[0]).toBe(false); + expect(tracked.emissions[1]).toBe(true); + expect(tracked.emissions[2]).toBe(false); + }); + + it("should cycle when filters are applied", async () => { + // Restart tracking + tracked = new ObservableTracker(service.loading$); + service.applyFilter("test"); + + await trackedCiphers.pauseUntilReceived(2); + + expect(tracked.emissions.length).toBe(3); + expect(tracked.emissions[0]).toBe(false); + expect(tracked.emissions[1]).toBe(true); + expect(tracked.emissions[2]).toBe(false); + }); + }); + describe("applyFilter", () => { it("should call search Service with the new search term", (done) => { const searchText = "Hello"; diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 189ce2c09f9..f96bb095b94 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -2,6 +2,7 @@ import { inject, Injectable, NgZone } from "@angular/core"; import { BehaviorSubject, combineLatest, + distinctUntilChanged, distinctUntilKeyChanged, from, map, @@ -12,6 +13,8 @@ import { startWith, Subject, switchMap, + tap, + withLatestFrom, } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; @@ -40,6 +43,13 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi export class VaultPopupItemsService { private _refreshCurrentTab$ = new Subject(); private _searchText$ = new BehaviorSubject(""); + + /** + * Subject that emits whenever new ciphers are being processed/filtered. + * @private + */ + private _ciphersLoading$ = new Subject(); + latestSearchText$: Observable = this._searchText$.asObservable(); /** @@ -84,6 +94,7 @@ export class VaultPopupItemsService { this.cipherService.localData$, ).pipe( runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular + tap(() => this._ciphersLoading$.next()), switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())), switchMap((ciphers) => combineLatest([ @@ -112,6 +123,7 @@ export class VaultPopupItemsService { this._searchText$, this.vaultPopupListFiltersService.filterFunction$, ]).pipe( + tap(() => this._ciphersLoading$.next()), map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [ filterFunction(ciphers), searchText, @@ -148,10 +160,8 @@ export class VaultPopupItemsService { * List of favorite ciphers that are not currently suggested for autofill. * Ciphers are sorted by last used date, then by name. */ - favoriteCiphers$: Observable = combineLatest([ - this.autoFillCiphers$, - this._filteredCipherList$, - ]).pipe( + favoriteCiphers$: Observable = this.autoFillCiphers$.pipe( + withLatestFrom(this._filteredCipherList$), map(([autoFillCiphers, ciphers]) => ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)), ), @@ -165,12 +175,9 @@ export class VaultPopupItemsService { * List of all remaining ciphers that are not currently suggested for autofill or marked as favorite. * Ciphers are sorted by name. */ - remainingCiphers$: Observable = combineLatest([ - this.autoFillCiphers$, - this.favoriteCiphers$, - this._filteredCipherList$, - ]).pipe( - map(([autoFillCiphers, favoriteCiphers, ciphers]) => + remainingCiphers$: Observable = this.favoriteCiphers$.pipe( + withLatestFrom(this._filteredCipherList$, this.autoFillCiphers$), + map(([favoriteCiphers, ciphers, autoFillCiphers]) => ciphers.filter( (cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher), ), @@ -179,6 +186,14 @@ export class VaultPopupItemsService { shareReplay({ refCount: false, bufferSize: 1 }), ); + /** + * Observable that indicates whether the service is currently loading ciphers. + */ + loading$: Observable = merge( + this._ciphersLoading$.pipe(map(() => true)), + this.remainingCiphers$.pipe(map(() => false)), + ).pipe(startWith(true), distinctUntilChanged(), shareReplay({ refCount: false, bufferSize: 1 })); + /** * Observable that indicates whether a filter is currently applied to the ciphers. */ diff --git a/libs/common/spec/observable-tracker.ts b/libs/common/spec/observable-tracker.ts index 9bf0475bee2..dfb49835933 100644 --- a/libs/common/spec/observable-tracker.ts +++ b/libs/common/spec/observable-tracker.ts @@ -1,4 +1,4 @@ -import { Observable, Subject, Subscription, firstValueFrom, throwError, timeout } from "rxjs"; +import { firstValueFrom, Observable, Subject, Subscription, throwError, timeout } from "rxjs"; /** Test class to enable async awaiting of observable emissions */ export class ObservableTracker { @@ -43,6 +43,9 @@ export class ObservableTracker { private trackEmissions(observable: Observable): T[] { const emissions: T[] = []; + this.emissionReceived.subscribe((value) => { + emissions.push(value); + }); this.subscription = observable.subscribe((value) => { if (value == null) { this.emissionReceived.next(null); @@ -64,9 +67,7 @@ export class ObservableTracker { } } }); - this.emissionReceived.subscribe((value) => { - emissions.push(value); - }); + return emissions; } } From b169207b74ee593365a82b1b73c6d0241a21928e Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 10 Jun 2024 11:59:20 -0500 Subject: [PATCH 08/12] [AC-2647] Remove Flexible Collections MVP code (#9518) * chore: organization.ts, remove refs to flexibleCollections and isManager, refs AC-2647 * chore: clean up callers of removed methods from organization.ts, refs AC-2647 * chore: access-selector, remove fc input and update permissionList param, refs AC-2647 * chore: update permissionList caller, update group-add-edit fc refs, and remove accessAll, refs AC-2647 * chore: update member-dialog fc callers, refs AC-2647 * chore: update bulk-collections-dialog fc callers, refs AC-2647 * chore: update collection-dialog fc callers, refs AC-2647 * chore: update simple fc caller to misc files, refs AC-2647 * chore: update member-dialog fc callers, refs AC-2647 * chore: remove accessAll references and update callers, refs AC-2647 * chore: update comment to specify v1 usage, refs AC-2647 * chore: remove unused message keys and code calls to use those messages, refs AC-2647 * chore: remove readonly false from access-selector model map function, refs AC-2647 --- .../core/services/group/group.service.ts | 1 - .../services/group/requests/group.request.ts | 1 - .../group/responses/group.response.ts | 6 - .../core/services/user-admin.service.ts | 3 - .../organizations/core/views/group.view.ts | 6 - .../views/organization-user-admin-view.ts | 6 - .../core/views/organization-user.view.ts | 6 - .../organization-layout.component.html | 2 +- .../manage/group-add-edit.component.html | 27 +-- .../manage/group-add-edit.component.ts | 13 +- .../manage/groups.component.html | 2 - .../member-dialog.component.html | 217 +++++------------- .../member-dialog/member-dialog.component.ts | 26 +-- .../members/people.component.html | 2 - .../settings/account.component.html | 2 +- .../access-selector.component.html | 9 - .../access-selector.component.ts | 20 +- .../access-selector/access-selector.models.ts | 16 +- .../access-selector.stories.ts | 3 - .../access-selector/user-type.pipe.ts | 2 - .../collection-dialog.component.html | 4 +- .../collection-dialog.component.ts | 14 +- .../vault-collection-row.component.ts | 2 +- .../vault-filter-section.component.ts | 14 +- .../bulk-collections-dialog.component.html | 2 - .../bulk-collections-dialog.component.ts | 6 +- .../vault-filter/vault-filter.component.ts | 6 +- .../vault-header/vault-header.component.html | 5 +- .../vault-header/vault-header.component.ts | 4 +- .../app/vault/org-vault/vault.component.html | 4 +- .../app/vault/org-vault/vault.component.ts | 2 +- apps/web/src/locales/en/messages.json | 27 --- libs/angular/src/pipes/user-type.pipe.ts | 2 - .../organization-user-invite.request.ts | 1 - .../organization-user-update.request.ts | 1 - .../responses/organization-user.response.ts | 6 - .../organization.service.abstraction.ts | 7 +- .../models/domain/organization.ts | 55 +---- .../src/vault/models/view/collection.view.ts | 14 +- .../src/components/import.component.ts | 7 +- .../src/components/export.component.ts | 6 +- 41 files changed, 106 insertions(+), 453 deletions(-) diff --git a/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts b/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts index 63431cd6abe..e06a9aa8dc7 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/group/group.service.ts @@ -80,7 +80,6 @@ export class InternalGroupService extends GroupService { async save(group: GroupView): Promise { const request = new GroupRequest(); request.name = group.name; - request.accessAll = group.accessAll; request.users = group.members; request.collections = group.collections.map( (c) => new SelectionReadOnlyRequest(c.id, c.readOnly, c.hidePasswords, c.manage), diff --git a/apps/web/src/app/admin-console/organizations/core/services/group/requests/group.request.ts b/apps/web/src/app/admin-console/organizations/core/services/group/requests/group.request.ts index b59c8696928..40f253d9452 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/group/requests/group.request.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/group/requests/group.request.ts @@ -2,7 +2,6 @@ import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models export class GroupRequest { name: string; - accessAll: boolean; collections: SelectionReadOnlyRequest[] = []; users: string[] = []; } diff --git a/apps/web/src/app/admin-console/organizations/core/services/group/responses/group.response.ts b/apps/web/src/app/admin-console/organizations/core/services/group/responses/group.response.ts index e969de4ad1f..eb62d83712f 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/group/responses/group.response.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/group/responses/group.response.ts @@ -5,11 +5,6 @@ export class GroupResponse extends BaseResponse { id: string; organizationId: string; name: string; - /** - * @deprecated - * To be removed after Flexible Collections. - **/ - accessAll: boolean; externalId: string; constructor(response: any) { @@ -17,7 +12,6 @@ export class GroupResponse extends BaseResponse { this.id = this.getResponseProperty("Id"); this.organizationId = this.getResponseProperty("OrganizationId"); this.name = this.getResponseProperty("Name"); - this.accessAll = this.getResponseProperty("AccessAll"); this.externalId = this.getResponseProperty("ExternalId"); } } diff --git a/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts b/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts index 399140e3ea6..52a522c89da 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts @@ -41,7 +41,6 @@ export class UserAdminService { async save(user: OrganizationUserAdminView): Promise { const request = new OrganizationUserUpdateRequest(); - request.accessAll = user.accessAll; request.permissions = user.permissions; request.type = user.type; request.collections = user.collections; @@ -54,7 +53,6 @@ export class UserAdminService { async invite(emails: string[], user: OrganizationUserAdminView): Promise { const request = new OrganizationUserInviteRequest(); request.emails = emails; - request.accessAll = user.accessAll; request.permissions = user.permissions; request.type = user.type; request.collections = user.collections; @@ -77,7 +75,6 @@ export class UserAdminService { view.type = u.type; view.status = u.status; view.externalId = u.externalId; - view.accessAll = u.accessAll; view.permissions = u.permissions; view.resetPasswordEnrolled = u.resetPasswordEnrolled; view.collections = u.collections.map((c) => ({ diff --git a/apps/web/src/app/admin-console/organizations/core/views/group.view.ts b/apps/web/src/app/admin-console/organizations/core/views/group.view.ts index 25864cca348..1909b9a863c 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/group.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/group.view.ts @@ -8,12 +8,6 @@ export class GroupView implements View { id: string; organizationId: string; name: string; - /** - * @deprecated - * To be removed after Flexible Collections. - * This will always return `false` if Flexible Collections is enabled. - **/ - accessAll: boolean; externalId: string; collections: CollectionAccessSelectionView[] = []; members: string[] = []; diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts index b4241826b3f..97e77d8543c 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts @@ -13,12 +13,6 @@ export class OrganizationUserAdminView { type: OrganizationUserType; status: OrganizationUserStatusType; externalId: string; - /** - * @deprecated - * To be removed after Flexible Collections. - * This will always return `false` if Flexible Collections is enabled. - **/ - accessAll: boolean; permissions: PermissionsApi; resetPasswordEnrolled: boolean; hasMasterPassword: boolean; diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts index 947ae9b13eb..86d1f4ded6b 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts @@ -12,12 +12,6 @@ export class OrganizationUserView { userId: string; type: OrganizationUserType; status: OrganizationUserStatusType; - /** - * @deprecated - * To be removed after Flexible Collections. - * This will always return `false` if Flexible Collections is enabled. - **/ - accessAll: boolean; permissions: PermissionsApi; resetPasswordEnrolled: boolean; name: string; diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 237e2c6e30c..445a0855c1b 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -11,7 +11,7 @@ diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index 166467ada09..eaf10405dbf 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -45,7 +45,6 @@ [columnHeader]="'member' | i18n" [selectorLabelText]="'selectMembers' | i18n" [emptySelectionText]="'noMembersAdded' | i18n" - [flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async" > @@ -56,24 +55,14 @@ {{ "restrictedCollectionAssignmentDesc" | i18n }}

-
- - -

{{ "accessAllCollectionsHelp" | i18n }}

-
- - - + diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index 38ef0025349..8df770686f4 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -96,9 +96,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private organization$ = this.organizationService .get$(this.organizationId) .pipe(shareReplay({ refCount: true })); - protected flexibleCollectionsEnabled$ = this.organization$.pipe( - map((o) => o?.flexibleCollections), - ); private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, ); @@ -114,7 +111,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { group: GroupView; groupForm = this.formBuilder.group({ - accessAll: [false], name: ["", [Validators.required, Validators.maxLength(100)]], externalId: this.formBuilder.control({ value: "", disabled: true }), members: [[] as AccessItemValue[]], @@ -188,7 +184,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { this.flexibleCollectionsV1Enabled$, ]).pipe( map(([organization, flexibleCollectionsV1Enabled]) => { - if (!flexibleCollectionsV1Enabled || !organization.flexibleCollections) { + if (!flexibleCollectionsV1Enabled) { return true; } @@ -276,7 +272,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { this.groupForm.patchValue({ name: this.group.name, externalId: this.group.externalId, - accessAll: this.group.accessAll, members: this.group.members.map((m) => ({ id: m, type: AccessItemType.Member, @@ -328,12 +323,8 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { const formValue = this.groupForm.value; groupView.name = formValue.name; - groupView.accessAll = formValue.accessAll; groupView.members = formValue.members?.map((m) => m.id) ?? []; - - if (!groupView.accessAll) { - groupView.collections = formValue.collections.map((c) => convertToSelectionView(c)); - } + groupView.collections = formValue.collections.map((c) => convertToSelectionView(c)); await this.groupService.save(groupView); diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.html b/apps/web/src/app/admin-console/organizations/manage/groups.component.html index f256c29b057..1a1a7cdb904 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.html @@ -74,12 +74,10 @@ - {{ "all" | i18n }} + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts new file mode 100644 index 00000000000..a3fad87c1b1 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -0,0 +1,64 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormsModule } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { SearchModule, ButtonModule } from "@bitwarden/components"; + +import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component"; + +@Component({ + selector: "app-add-edit-v2", + templateUrl: "add-edit-v2.component.html", + standalone: true, + imports: [ + CommonModule, + SearchModule, + JslibModule, + FormsModule, + ButtonModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + ], +}) +export class AddEditV2Component { + headerText: string; + + constructor( + private route: ActivatedRoute, + private i18nService: I18nService, + ) { + this.subscribeToParams(); + } + + subscribeToParams(): void { + this.route.queryParams.pipe(takeUntilDestroyed()).subscribe((params) => { + const isNew = params.isNew.toLowerCase() === "true"; + const cipherType = parseInt(params.type); + + this.headerText = this.setHeader(isNew, cipherType); + }); + } + + setHeader(isNew: boolean, type: CipherType) { + const partOne = isNew ? "newItemHeader" : "editItemHeader"; + + switch (type) { + case CipherType.Login: + return this.i18nService.t(partOne, this.i18nService.t("typeLogin")); + case CipherType.Card: + return this.i18nService.t(partOne, this.i18nService.t("typeCard")); + case CipherType.Identity: + return this.i18nService.t(partOne, this.i18nService.t("typeIdentity")); + case CipherType.SecureNote: + return this.i18nService.t(partOne, this.i18nService.t("note")); + } + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html new file mode 100644 index 00000000000..0bd85c21696 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html @@ -0,0 +1,22 @@ + + + + + {{ "typeLogin" | i18n }} + + + + {{ "typeCard" | i18n }} + + + + {{ "typeIdentity" | i18n }} + + + + {{ "note" | i18n }} + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts new file mode 100644 index 00000000000..e90afec5388 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -0,0 +1,28 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Router, RouterLink } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { ButtonModule, NoItemsModule, MenuModule } from "@bitwarden/components"; + +@Component({ + selector: "app-new-item-dropdown", + templateUrl: "new-item-dropdown-v2.component.html", + standalone: true, + imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule], +}) +export class NewItemDropdownV2Component implements OnInit, OnDestroy { + cipherType = CipherType; + + constructor(private router: Router) {} + + ngOnInit(): void {} + + ngOnDestroy(): void {} + + // TODO PM-6826: add selectedVault query param + newItemNavigate(type: CipherType) { + void this.router.navigate(["/add-cipher"], { queryParams: { type: type, isNew: true } }); + } +} diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html index 694c0e9be52..7dd06310159 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html @@ -1,11 +1,8 @@ - - - - {{ "new" | i18n }} - + + @@ -18,9 +15,7 @@ {{ "yourVaultIsEmpty" | i18n }} {{ "autofillSuggestionsTip" | i18n }} - + diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts index f6f6872c1c5..9939727806b 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts @@ -5,6 +5,7 @@ import { Router, RouterLink } from "@angular/router"; import { combineLatest } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CipherType } from "@bitwarden/common/vault/enums"; import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components"; import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component"; @@ -13,6 +14,7 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2"; +import { NewItemDropdownV2Component } from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component"; import { VaultListFiltersComponent } from "../vault-v2/vault-list-filters/vault-list-filters.component"; import { VaultV2SearchComponent } from "../vault-v2/vault-search/vault-v2-search.component"; @@ -40,9 +42,11 @@ enum VaultState { ButtonModule, RouterLink, VaultV2SearchComponent, + NewItemDropdownV2Component, ], }) export class VaultV2Component implements OnInit, OnDestroy { + cipherType = CipherType; protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$; protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$; @@ -86,9 +90,4 @@ export class VaultV2Component implements OnInit, OnDestroy { ngOnInit(): void {} ngOnDestroy(): void {} - - addCipher() { - // TODO: Add currently filtered organization to query params if available - void this.router.navigate(["/add-cipher"], {}); - } } diff --git a/apps/browser/src/vault/popup/components/vault/view.component.ts b/apps/browser/src/vault/popup/components/vault/view.component.ts index a225db0c11a..211bd8fc099 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view.component.ts @@ -198,7 +198,9 @@ export class ViewComponent extends BaseViewComponent { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/edit-cipher"], { queryParams: { cipherId: this.cipher.id } }); + this.router.navigate(["/edit-cipher"], { + queryParams: { cipherId: this.cipher.id, type: this.cipher.type, isNew: false }, + }); return true; } From d594b680f927003c62b901d4a57350ace467c155 Mon Sep 17 00:00:00 2001 From: Dillon Beresford <165616268+bwdil@users.noreply.github.com> Date: Mon, 10 Jun 2024 13:24:56 -0500 Subject: [PATCH 10/12] [PM-7025] Add permission for test results (#9569) * include check-run in workflows where secrets are used * revert changes in build-cli workflow and add check-run to codecov * assert token permissions * include required permissions * re-arrange permissions in alphabetical order --------- Co-authored-by: Matt Bishop --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cb4a18947be..7d841ca880e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,7 @@ jobs: runs-on: ubuntu-22.04 needs: check-run permissions: + checks: write contents: read pull-requests: write From cbc34950fb9a52f4c9b64ba96d3a5abaad4fea41 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 10 Jun 2024 14:25:21 -0500 Subject: [PATCH 11/12] add check for PersonalOwnershipPolicy in vault filters (#9570) --- .../vault-popup-list-filters.service.spec.ts | 72 +++++++++++++++ .../vault-popup-list-filters.service.ts | 92 ++++++++++++------- 2 files changed, 129 insertions(+), 35 deletions(-) diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 907ff9af8d6..b89de79a209 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -3,6 +3,8 @@ import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, skipWhile } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,6 +25,7 @@ describe("VaultPopupListFiltersService", () => { const folderViews$ = new BehaviorSubject([]); const cipherViews$ = new BehaviorSubject({}); const decryptedCollections$ = new BehaviorSubject([]); + const policyAppliesToActiveUser$ = new BehaviorSubject(false); const collectionService = { decryptedCollections$, @@ -45,9 +48,15 @@ describe("VaultPopupListFiltersService", () => { t: (key: string) => key, } as I18nService; + const policyService = { + policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$), + }; + beforeEach(() => { memberOrganizations$.next([]); decryptedCollections$.next([]); + policyAppliesToActiveUser$.next(false); + policyService.policyAppliesToActiveUser$.mockClear(); collectionService.getAllNested = () => Promise.resolve([]); TestBed.configureTestingModule({ @@ -72,6 +81,10 @@ describe("VaultPopupListFiltersService", () => { provide: CollectionService, useValue: collectionService, }, + { + provide: PolicyService, + useValue: policyService, + }, { provide: FormBuilder, useClass: FormBuilder }, ], }); @@ -127,6 +140,65 @@ describe("VaultPopupListFiltersService", () => { }); }); + describe("PersonalOwnership policy", () => { + it('calls policyAppliesToActiveUser$ with "PersonalOwnership"', () => { + expect(policyService.policyAppliesToActiveUser$).toHaveBeenCalledWith( + PolicyType.PersonalOwnership, + ); + }); + + it("returns an empty array when the policy applies and there is a single organization", (done) => { + policyAppliesToActiveUser$.next(true); + memberOrganizations$.next([ + { name: "bobby's org", id: "1234-3323-23223" }, + ] as Organization[]); + + service.organizations$.subscribe((organizations) => { + expect(organizations).toEqual([]); + done(); + }); + }); + + it('adds "myVault" when the policy does not apply and there are multiple organizations', (done) => { + policyAppliesToActiveUser$.next(false); + const orgs = [ + { name: "bobby's org", id: "1234-3323-23223" }, + { name: "alice's org", id: "2223-4343-99888" }, + ] as Organization[]; + + memberOrganizations$.next(orgs); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual([ + "myVault", + "alice's org", + "bobby's org", + ]); + done(); + }); + }); + + it('does not add "myVault" the policy applies and there are multiple organizations', (done) => { + policyAppliesToActiveUser$.next(true); + const orgs = [ + { name: "bobby's org", id: "1234-3323-23223" }, + { name: "alice's org", id: "2223-3242-99888" }, + { name: "catherine's org", id: "77733-4343-99888" }, + ] as Organization[]; + + memberOrganizations$.next(orgs); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual([ + "alice's org", + "bobby's org", + "catherine's org", + ]); + done(); + }); + }); + }); + describe("icons", () => { it("sets family icon for family organizations", (done) => { const orgs = [ diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 6406e43446d..66e264dd6de 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -13,6 +13,8 @@ import { import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -88,6 +90,7 @@ export class VaultPopupListFiltersService { private i18nService: I18nService, private collectionService: CollectionService, private formBuilder: FormBuilder, + private policyService: PolicyService, ) { this.filterForm.controls.organization.valueChanges .pipe(takeUntilDestroyed()) @@ -167,44 +170,63 @@ export class VaultPopupListFiltersService { /** * Organization array structured to be directly passed to `ChipSelectComponent` */ - organizations$: Observable[]> = - this.organizationService.memberOrganizations$.pipe( - map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))), - map((orgs) => { - if (!orgs.length) { - return []; - } + organizations$: Observable[]> = combineLatest([ + this.organizationService.memberOrganizations$, + this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), + ]).pipe( + map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ + orgs.sort(Utils.getSortFunction(this.i18nService, "name")), + personalOwnershipApplies, + ]), + map(([orgs, personalOwnershipApplies]) => { + // When there are no organizations return an empty array, + // resulting in the org filter being hidden + if (!orgs.length) { + return []; + } - return [ - // When the user is a member of an organization, make the "My Vault" option available - { - value: { id: MY_VAULT_ID } as Organization, - label: this.i18nService.t("myVault"), - icon: "bwi-user", - }, - ...orgs.map((org) => { - let icon = "bwi-business"; + // When there is only one organization and personal ownership policy applies, + // return an empty array, resulting in the org filter being hidden + if (orgs.length === 1 && personalOwnershipApplies) { + return []; + } - if (!org.enabled) { - // Show a warning icon if the organization is deactivated - icon = "bwi-exclamation-triangle tw-text-danger"; - } else if ( - org.planProductType === ProductType.Families || - org.planProductType === ProductType.Free - ) { - // Show a family icon if the organization is a family or free org - icon = "bwi-family"; - } + const myVaultOrg: ChipSelectOption[] = []; - return { - value: org, - label: org.name, - icon, - }; - }), - ]; - }), - ); + // Only add "My vault" if personal ownership policy does not apply + if (!personalOwnershipApplies) { + myVaultOrg.push({ + value: { id: MY_VAULT_ID } as Organization, + label: this.i18nService.t("myVault"), + icon: "bwi-user", + }); + } + + return [ + ...myVaultOrg, + ...orgs.map((org) => { + let icon = "bwi-business"; + + if (!org.enabled) { + // Show a warning icon if the organization is deactivated + icon = "bwi-exclamation-triangle tw-text-danger"; + } else if ( + org.planProductType === ProductType.Families || + org.planProductType === ProductType.Free + ) { + // Show a family icon if the organization is a family or free org + icon = "bwi-family"; + } + + return { + value: org, + label: org.name, + icon, + }; + }), + ]; + }), + ); /** * Folder array structured to be directly passed to `ChipSelectComponent` From f9faeeba4c9a928cf00936951484c6673786cf9d Mon Sep 17 00:00:00 2001 From: Opeyemi Date: Tue, 11 Jun 2024 12:03:04 +0100 Subject: [PATCH 12/12] restrict deployment to USDEV and protect environment (#9571) * restrict deployment to USDEV and protect environment * remove converting env name to lower char --- .github/workflows/deploy-web.yml | 62 +++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 1ff67671419..52230a12bcc 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -112,13 +112,48 @@ jobs: echo "azure-login-creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT echo "retrieve-secrets-keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT echo "environment-artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT + echo "environment-name=Web Vault - US DEV Cloud" >> $GITHUB_OUTPUT echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT ;; esac # Set the sync utility to use for deployment to the environment (az-sync or azcopy) echo "sync-utility=azcopy" >> $GITHUB_OUTPUT + - name: Environment Protection + env: + TAG: ${{ steps.project_tag.outputs.tag }} + run: | + BRANCH_OR_TAG_LOWER=$(echo ${{ inputs.branch-or-tag }} | awk '{print tolower($0)}') + + PROD_ENV_PATTERN='USPROD|EUPROD' + PROD_ALLOWED_TAGS_PATTERN='web-v[0-9]+\.[0-9]+\.[0-9]+' + + QA_ENV_PATTERN='USQA|EUQA' + QA_ALLOWED_TAGS_PATTERN='.*' + + DEV_ENV_PATTERN='USDEV' + DEV_ALLOWED_TAGS_PATTERN='.*' + + if [[ \ + ${{ inputs.environment }} =~ \.*($PROD_ENV_PATTERN)\.* && \ + ! "$BRANCH_OR_TAG_LOWER" =~ ^($PROD_ALLOWED_TAGS_PATTERN).* \ + ]] || [[ \ + ${{ inputs.environment }} =~ \.*($QA_ENV_PATTERN)\.* && \ + ! "$BRANCH_OR_TAG_LOWER" =~ ^($QA_ALLOWED_TAGS_PATTERN).* \ + ]] || [[ \ + =~ \.*($DEV_ENV_PATTERN)\.* && \ + ! "$BRANCH_OR_TAG_LOWER" =~ ^($DEV_ALLOWED_TAGS_PATTERN).* \ + ]]; then + echo "!Deployment blocked!" + echo "Attempting to deploy a tag that is not allowed in ${{ inputs.environment }} environment" + echo + echo "Environment: ${{ inputs.environment }} + echo "Tag: ${{ inputs.branch-or-tag }} + exit 1 + else + echo "${{ inputs.branch-or-tag }} is allowed to deployed on to ${{ inputs.environment }} environment" + fi + approval: name: Approval for Deployment to ${{ needs.setup.outputs.environment-name }} needs: setup @@ -206,6 +241,31 @@ jobs: echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT fi + - name: Ensure artifact is from main branch for USDEV environment + if: ${{ 'inputs.environment' == 'USDEV'}} + run: | + # If run-id was used + if [ "${{ inputs.build-web-run-id }}" ]; then + if [ "${{ steps.download-latest-artifacts.outputs.artifact-build-branch }}" != "main" ]; then + echo "Artifact is not from main branch" + exit 1 + fi + + # If artifact download failed + elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then + branch=$(gh api /repos/bitwarden/clients/actions/runs/${{ steps.trigger-build-web.outputs.workflow_id }}/artifacts --jq '.artifacts[0].workflow_run.head_branch') + if [ "$branch" != "main" ]; then + echo "Artifact is not from main branch" + exit 1 + fi + + else + if [ "${{ steps.download-latest-artifacts.outputs.artifact-build-branch }}" != "main" ]; then + echo "Artifact is not from main branch" + exit 1 + fi + fi + notify-start: name: Notify Slack with start message needs: