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: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12649b91ea9..7d841ca880e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,16 +8,27 @@ 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: + checks: write + contents: read + pull-requests: write + steps: - name: Checkout repo uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index f9bf1bc523e..ecea2deb9ef 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1434,6 +1434,24 @@ "typeIdentity": { "message": "Identity" }, + "newItemHeader":{ + "message": "New $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, + "editItemHeader":{ + "message": "Edit $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 97008ab96d9..1109ab73adf 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -57,6 +57,7 @@ import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filt import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; +import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component"; @@ -195,20 +196,18 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "cipher-password-history" }, }, - { + ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "add-cipher", - component: AddEditComponent, canActivate: [AuthGuard, debounceNavigationGuard()], data: { state: "add-cipher" }, runGuardsAndResolvers: "always", - }, - { + }), + ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "edit-cipher", - component: AddEditComponent, canActivate: [AuthGuard, debounceNavigationGuard()], data: { state: "edit-cipher" }, runGuardsAndResolvers: "always", - }, + }), { path: "share-cipher", component: ShareComponent, diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html new file mode 100644 index 00000000000..09b764cbc8f --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -0,0 +1,9 @@ + + + + + + + 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; } 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/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` 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/apps/cli/src/admin-console/models/request/organization-collection.request.ts b/apps/cli/src/admin-console/models/request/organization-collection.request.ts index 7546d116092..1bb7a24ce77 100644 --- a/apps/cli/src/admin-console/models/request/organization-collection.request.ts +++ b/apps/cli/src/admin-console/models/request/organization-collection.request.ts @@ -9,8 +9,10 @@ export class OrganizationCollectionRequest extends CollectionExport { req.name = "Collection name"; req.externalId = null; req.groups = [SelectionReadOnly.template(), SelectionReadOnly.template()]; + req.users = [SelectionReadOnly.template(), SelectionReadOnly.template()]; return req; } groups: SelectionReadOnly[]; + users: SelectionReadOnly[]; } diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index e64ff8b5512..75cd241207c 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -170,10 +170,17 @@ export class EditCommand { : req.groups.map( (g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage), ); + const users = + req.users == null + ? null + : req.users.map( + (u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage), + ); const request = new CollectionRequest(); request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString; request.externalId = req.externalId; request.groups = groups; + request.users = users; const response = await this.apiService.putCollection(req.organizationId, id, request); const view = CollectionExport.toView(req); view.id = response.id; diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 2b1e21f8abb..8949e5b71e5 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -87,6 +87,7 @@ export class ServeCommand { this.serviceContainer.apiService, this.serviceContainer.folderApiService, this.serviceContainer.billingAccountProfileStateService, + this.serviceContainer.organizationService, ); this.editCommand = new EditCommand( this.serviceContainer.cipherService, diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index c8e0701845b..04ca47ac1e1 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -226,6 +226,7 @@ export class VaultProgram extends BaseProgram { this.serviceContainer.apiService, this.serviceContainer.folderApiService, this.serviceContainer.billingAccountProfileStateService, + this.serviceContainer.organizationService, ); const response = await command.run(object, encodedJson, cmd); this.processResponse(response); diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 78ee04e73c0..716c2b42bb1 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -4,6 +4,7 @@ import * as path from "path"; import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; @@ -32,6 +33,7 @@ export class CreateCommand { private apiService: ApiService, private folderApiService: FolderApiServiceAbstraction, private accountProfileService: BillingAccountProfileStateService, + private organizationService: OrganizationService, ) {} async run( @@ -183,6 +185,8 @@ export class CreateCommand { if (orgKey == null) { throw new Error("No encryption key for this organization."); } + const organization = await this.organizationService.get(req.organizationId); + const currentOrgUserId = organization.organizationUserId; const groups = req.groups == null @@ -190,10 +194,17 @@ export class CreateCommand { : req.groups.map( (g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage), ); + const users = + req.users == null + ? [new SelectionReadOnlyRequest(currentOrgUserId, false, false, true)] + : req.users.map( + (u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage), + ); const request = new CollectionRequest(); request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString; request.externalId = req.externalId; request.groups = groups; + request.users = users; const response = await this.apiService.postCollection(req.organizationId, request); const view = CollectionExport.toView(req); view.id = response.id; 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 }} - - - - - +
+

{{ "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, + }, ], }, { diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html index 3f5624c0c97..0640681cf44 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html @@ -64,7 +64,7 @@ -
+
{{ "readOnlyCollectionAccess" | i18n }} @@ -107,7 +107,6 @@ [selectorLabelText]="'selectGroupsAndMembers' | i18n" [selectorHelpText]="'userPermissionOverrideHelperDesc' | i18n" [emptySelectionText]="'noMembersOrGroupsAdded' | i18n" - [flexibleCollectionsEnabled]="organization.flexibleCollections" > diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 8040cf13cdd..6b90480560f 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -223,7 +223,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { (u) => u.userId === this.organization?.userId, )?.id; const initialSelection: AccessItemValue[] = - currentOrgUserId !== undefined && organization.flexibleCollections + currentOrgUserId !== undefined ? [ { id: currentOrgUserId, @@ -239,11 +239,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { }); } - if ( - organization.flexibleCollections && - flexibleCollectionsV1 && - !organization.allowAdminAccessToAllCollectionItems - ) { + if (flexibleCollectionsV1 && !organization.allowAdminAccessToAllCollectionItems) { this.formGroup.controls.access.addValidators(validateCanManagePermission); } else { this.formGroup.controls.access.removeValidators(validateCanManagePermission); @@ -444,8 +440,7 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces type: AccessItemType.Group, listName: group.name, labelName: group.name, - accessAllItems: group.accessAll, - readonly: group.accessAll, + readonly: false, readonlyPermission: collectionId != null ? convertToPermission(group.collections.find((gc) => gc.id == collectionId)) @@ -471,8 +466,7 @@ function mapUserToAccessItemView( listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email, labelName: user.name ?? user.email, status: user.status, - accessAllItems: user.accessAll, - readonly: user.accessAll, + readonly: false, readonlyPermission: collectionId != null ? convertToPermission( diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 67bbf7b3806..8909c3e8bd9 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -86,7 +86,7 @@ export class VaultCollectionRowComponent { return this.i18nService.t("canEdit"); } if ((this.collection as CollectionAdminView).assigned) { - const permissionList = getPermissionList(this.organization?.flexibleCollections); + const permissionList = getPermissionList(); return this.i18nService.t( permissionList.find((p) => p.perm === convertToPermission(this.collection))?.labelId, ); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts index 305212b77ca..93c2c7a671f 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts @@ -15,7 +15,6 @@ import { VaultFilter } from "../models/vault-filter.model"; }) export class VaultFilterSectionComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); - protected flexibleCollectionsEnabled: boolean; @Input() activeFilter: VaultFilter; @Input() section: VaultFilterSection; @@ -40,12 +39,6 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy { this.section?.data$?.pipe(takeUntil(this.destroy$)).subscribe((data) => { this.data = data; }); - this.vaultFilterService - .getOrganizationFilter() - .pipe(takeUntil(this.destroy$)) - .subscribe((org) => { - this.flexibleCollectionsEnabled = org != null ? org.flexibleCollections : false; - }); } ngOnDestroy() { @@ -77,10 +70,9 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy { const { organizationId, cipherTypeId, folderId, collectionId, isCollectionSelected } = this.activeFilter; - const collectionStatus = this.flexibleCollectionsEnabled - ? filterNode?.node.id === "AllCollections" && - (isCollectionSelected || collectionId === "AllCollections") - : collectionId === filterNode?.node.id; + const collectionStatus = + filterNode?.node.id === "AllCollections" && + (isCollectionSelected || collectionId === "AllCollections"); return ( organizationId === filterNode?.node.id || diff --git a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.html b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.html index 9ce066f06e0..80a1c75296c 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.html +++ b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.html @@ -17,7 +17,6 @@ [selectorLabelText]="'selectGroupsAndMembers' | i18n" [selectorHelpText]="'userPermissionOverrideHelperDesc' | i18n" [emptySelectionText]="'noMembersOrGroupsAdded' | i18n" - [flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async" >
diff --git a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts index 81b7f3c4276..0d55a30e620 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -1,7 +1,7 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { combineLatest, map, of, Subject, switchMap, takeUntil } from "rxjs"; +import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; @@ -42,10 +42,6 @@ export enum BulkCollectionsDialogResult { standalone: true, }) export class BulkCollectionsDialogComponent implements OnDestroy { - protected flexibleCollectionsEnabled$ = this.organizationService - .get$(this.params.organizationId) - .pipe(map((o) => o?.flexibleCollections)); - protected readonly PermissionMode = PermissionMode; protected formGroup = this.formBuilder.group({ diff --git a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts index 16302716976..a7a583cd526 100644 --- a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.component.ts @@ -103,11 +103,7 @@ export class VaultFilterComponent extends BaseVaultFilterComponent implements On async buildAllFilters(): Promise { const builderFilter = {} as VaultFilterList; builderFilter.typeFilter = await this.addTypeFilter(["favorites"]); - if (this._organization?.flexibleCollections) { - builderFilter.collectionFilter = await this.addCollectionFilter(); - } else { - builderFilter.collectionFilter = await super.addCollectionFilter(); - } + builderFilter.collectionFilter = await this.addCollectionFilter(); builderFilter.trashFilter = await this.addTrashFilter(); return builderFilter; } diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html index 426f1812c99..ee45f5921e5 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html @@ -6,10 +6,7 @@ queryParamsHandling="merge" > {{ organization.name }} - - {{ "vault" | i18n | lowercase }} - - + {{ "collections" | i18n | lowercase }} diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 1025745ee91..b22751c0430 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -89,9 +89,7 @@ export class VaultHeaderComponent implements OnInit { } get title() { - const headerType = this.organization?.flexibleCollections - ? this.i18nService.t("collections").toLowerCase() - : this.i18nService.t("vault").toLowerCase(); + const headerType = this.i18nService.t("collections").toLowerCase(); if (this.collection != null) { return this.collection.node.name; diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index af8789c906c..d2715ce10d9 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -65,8 +65,8 @@ [useEvents]="organization?.canAccessEventLogs" [showAdminActions]="true" (onEvent)="onVaultItemsEvent($event)" - [showBulkEditCollectionAccess]="organization?.flexibleCollections" - [showBulkAddToCollections]="organization?.flexibleCollections" + [showBulkEditCollectionAccess]="true" + [showBulkAddToCollections]="true" [viewingOrgVault]="true" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" [addAccessStatus]="addAccessStatus$ | async" diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index dfdce5c818e..c0322e82df5 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -156,7 +156,7 @@ export class VaultComponent implements OnInit, OnDestroy { private _flexibleCollectionsV1FlagEnabled: boolean; protected get flexibleCollectionsV1Enabled(): boolean { - return this._flexibleCollectionsV1FlagEnabled && this.organization?.flexibleCollections; + return this._flexibleCollectionsV1FlagEnabled; } protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index d7a21ad6d6a..39423cbd905 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2794,12 +2794,6 @@ "userDesc": { "message": "Access and add items to assigned collections" }, - "manager": { - "message": "Manager" - }, - "managerDesc": { - "message": "Create, delete, and manage access in assigned collections" - }, "all": { "message": "All" }, @@ -4576,12 +4570,6 @@ "permission": { "message": "Permission" }, - "managerPermissions": { - "message": "Manager Permissions" - }, - "adminPermissions": { - "message": "Admin Permissions" - }, "accessEventLogs": { "message": "Access event logs" }, @@ -4606,9 +4594,6 @@ "deleteAnyCollection": { "message": "Delete any collection" }, - "manageAssignedCollections": { - "message": "Manage assigned collections" - }, "editAssignedCollections": { "message": "Edit assigned collections" }, @@ -6669,12 +6654,6 @@ "restrictedCollectionAssignmentDesc": { "message": "You can only assign collections you manage." }, - "accessAllCollectionsDesc": { - "message": "Grant access to all current and future collections." - }, - "accessAllCollectionsHelp": { - "message": "If checked, this will replace all other collection permissions." - }, "selectMembers": { "message": "Select members" }, @@ -6717,12 +6696,6 @@ "group": { "message": "Group" }, - "groupAccessAll": { - "message": "This group can access and modify all items." - }, - "memberAccessAll": { - "message": "This member can access and modify all items." - }, "domainVerification": { "message": "Domain verification" }, @@ -8354,5 +8327,8 @@ }, "verified": { "message": "Verified" + }, + "viewSecret": { + "message": "View secret" } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/access-policy.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/access-policy.view.ts index 76be88b610e..0863bb0cccf 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/access-policy.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/access-policy.view.ts @@ -1,44 +1,26 @@ -export class BaseAccessPolicyView { - id: string; +class BaseAccessPolicyView { read: boolean; write: boolean; - creationDate: string; - revisionDate: string; } -export class UserProjectAccessPolicyView extends BaseAccessPolicyView { +export class UserAccessPolicyView extends BaseAccessPolicyView { organizationUserId: string; organizationUserName: string; - grantedProjectId: string; - userId: string; currentUser: boolean; } -export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView { - organizationUserId: string; - organizationUserName: string; - grantedServiceAccountId: string; - userId: string; - currentUser: boolean; -} - -export class GroupProjectAccessPolicyView extends BaseAccessPolicyView { +export class GroupAccessPolicyView extends BaseAccessPolicyView { groupId: string; groupName: string; - grantedProjectId: string; currentUserInGroup: boolean; } -export class GroupServiceAccountAccessPolicyView extends BaseAccessPolicyView { - groupId: string; - groupName: string; - grantedServiceAccountId: string; - currentUserInGroup: boolean; -} - -export class ServiceAccountProjectAccessPolicyView extends BaseAccessPolicyView { +export class ServiceAccountAccessPolicyView extends BaseAccessPolicyView { serviceAccountId: string; serviceAccountName: string; +} + +export class GrantedProjectAccessPolicyView extends BaseAccessPolicyView { grantedProjectId: string; grantedProjectName: string; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-people-access-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-people-access-policies.view.ts index c85b289928b..9a35e76a615 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-people-access-policies.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-people-access-policies.view.ts @@ -1,6 +1,6 @@ -import { GroupProjectAccessPolicyView, UserProjectAccessPolicyView } from "./access-policy.view"; +import { GroupAccessPolicyView, UserAccessPolicyView } from "./access-policy.view"; export class ProjectPeopleAccessPoliciesView { - userAccessPolicies: UserProjectAccessPolicyView[]; - groupAccessPolicies: GroupProjectAccessPolicyView[]; + userAccessPolicies: UserAccessPolicyView[]; + groupAccessPolicies: GroupAccessPolicyView[]; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-service-accounts-access-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-service-accounts-access-policies.view.ts index 28636c4df1d..9faaa29b697 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-service-accounts-access-policies.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/project-service-accounts-access-policies.view.ts @@ -1,5 +1,5 @@ -import { ServiceAccountProjectAccessPolicyView } from "./access-policy.view"; +import { ServiceAccountAccessPolicyView } from "./access-policy.view"; export class ProjectServiceAccountsAccessPoliciesView { - serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[]; + serviceAccountAccessPolicies: ServiceAccountAccessPolicyView[]; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/secret-access-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/secret-access-policies.view.ts new file mode 100644 index 00000000000..8742021a421 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/secret-access-policies.view.ts @@ -0,0 +1,11 @@ +import { + GroupAccessPolicyView, + UserAccessPolicyView, + ServiceAccountAccessPolicyView, +} from "./access-policy.view"; + +export class SecretAccessPoliciesView { + userAccessPolicies: UserAccessPolicyView[]; + groupAccessPolicies: GroupAccessPolicyView[]; + serviceAccountAccessPolicies: ServiceAccountAccessPolicyView[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-granted-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-granted-policies.view.ts index 7d53e38263a..e055daa199a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-granted-policies.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-granted-policies.view.ts @@ -1,10 +1,10 @@ -import { ServiceAccountProjectAccessPolicyView } from "./access-policy.view"; +import { GrantedProjectAccessPolicyView } from "./access-policy.view"; export class ServiceAccountGrantedPoliciesView { - grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[]; + grantedProjectPolicies: GrantedProjectPolicyPermissionDetailsView[]; } -export class ServiceAccountProjectPolicyPermissionDetailsView { - accessPolicy: ServiceAccountProjectAccessPolicyView; +export class GrantedProjectPolicyPermissionDetailsView { + accessPolicy: GrantedProjectAccessPolicyView; hasPermission: boolean; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-people-access-policies.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-people-access-policies.view.ts index 58dcf6d4706..2ef4eedc33e 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-people-access-policies.view.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policies/service-account-people-access-policies.view.ts @@ -1,9 +1,6 @@ -import { - GroupServiceAccountAccessPolicyView, - UserServiceAccountAccessPolicyView, -} from "./access-policy.view"; +import { GroupAccessPolicyView, UserAccessPolicyView } from "./access-policy.view"; export class ServiceAccountPeopleAccessPoliciesView { - userAccessPolicies: UserServiceAccountAccessPolicyView[]; - groupAccessPolicies: GroupServiceAccountAccessPolicyView[]; + userAccessPolicies: UserAccessPolicyView[]; + groupAccessPolicies: GroupAccessPolicyView[]; } 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-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts index c49008c580b..0b0e13fe1f5 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts @@ -12,7 +12,7 @@ import { DialogService } from "@bitwarden/components"; import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service"; import { ApItemValueType, - convertToProjectPeopleAccessPoliciesView, + convertToPeopleAccessPoliciesView, } from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type"; import { ApItemViewType, @@ -119,10 +119,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy { } try { - const projectPeopleView = convertToProjectPeopleAccessPoliciesView( - this.projectId, - formValues, - ); + const projectPeopleView = convertToPeopleAccessPoliciesView(formValues); const peoplePoliciesViews = await this.accessPolicyService.putProjectPeopleAccessPolicies( this.projectId, projectPeopleView, 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/projects/project/project-service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts index 7ac111ef620..1069033b57d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-service-accounts.component.ts @@ -144,7 +144,7 @@ export class ProjectServiceAccountsComponent implements OnInit, OnDestroy { projectId: string, selectedPolicies: ApItemValueType[], ): Promise { - const view = convertToProjectServiceAccountsAccessPoliciesView(projectId, selectedPolicies); + const view = convertToProjectServiceAccountsAccessPoliciesView(selectedPolicies); return await this.accessPolicyService.putProjectServiceAccountsAccessPolicies( organizationId, projectId, 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/service-accounts/people/service-account-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts index a3d3984ea82..31394f7fec7 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts @@ -11,7 +11,7 @@ import { DialogService } from "@bitwarden/components"; import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service"; import { ApItemValueType, - convertToServiceAccountPeopleAccessPoliciesView, + convertToPeopleAccessPoliciesView, } from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type"; import { ApItemViewType, @@ -180,10 +180,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { serviceAccountId: string, selectedPolicies: ApItemValueType[], ) { - const serviceAccountPeopleView = convertToServiceAccountPeopleAccessPoliciesView( - serviceAccountId, - selectedPolicies, - ); + const serviceAccountPeopleView = convertToPeopleAccessPoliciesView(selectedPolicies); return await this.accessPolicyService.putServiceAccountPeopleAccessPolicies( serviceAccountId, serviceAccountPeopleView, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts index 49ddfe331fb..358a152b6f3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/projects/service-account-projects.component.ts @@ -144,10 +144,7 @@ export class ServiceAccountProjectsComponent implements OnInit, OnDestroy { serviceAccountId: string, selectedPolicies: ApItemValueType[], ): Promise { - const grantedViews = convertToServiceAccountGrantedPoliciesView( - serviceAccountId, - selectedPolicies, - ); + const grantedViews = convertToServiceAccountGrantedPoliciesView(selectedPolicies); return await this.accessPolicyService.putServiceAccountGrantedPolicies( organizationId, serviceAccountId, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.spec.ts new file mode 100644 index 00000000000..ad6564a1c6c --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.spec.ts @@ -0,0 +1,238 @@ +import { + convertToSecretAccessPoliciesView, + convertToPeopleAccessPoliciesView, + ApItemValueType, + convertToProjectServiceAccountsAccessPoliciesView, + convertToServiceAccountGrantedPoliciesView, +} from "./ap-item-value.type"; +import { ApItemEnum } from "./enums/ap-item.enum"; +import { ApPermissionEnum } from "./enums/ap-permission.enum"; + +describe("convertToPeopleAccessPoliciesView", () => { + it("should convert selected policy values to user and group access policies view", () => { + const selectedPolicyValues = [...createUserApItems(), ...createGroupApItems()]; + + const result = convertToPeopleAccessPoliciesView(selectedPolicyValues); + + expect(result.userAccessPolicies).toEqual(expectedUserAccessPolicies); + expect(result.groupAccessPolicies).toEqual(expectedGroupAccessPolicies); + }); + + it("should return empty user array if no selected users are provided", () => { + const selectedPolicyValues = createGroupApItems(); + + const result = convertToPeopleAccessPoliciesView(selectedPolicyValues); + + expect(result.userAccessPolicies).toEqual([]); + expect(result.groupAccessPolicies).toEqual(expectedGroupAccessPolicies); + }); + + it("should return empty group array if no selected groups are provided", () => { + const selectedPolicyValues = createUserApItems(); + + const result = convertToPeopleAccessPoliciesView(selectedPolicyValues); + + expect(result.userAccessPolicies).toEqual(expectedUserAccessPolicies); + expect(result.groupAccessPolicies).toEqual([]); + }); + + it("should return empty arrays if no selected policy values are provided", () => { + const selectedPolicyValues: ApItemValueType[] = []; + + const result = convertToPeopleAccessPoliciesView(selectedPolicyValues); + + expect(result.userAccessPolicies).toEqual([]); + expect(result.groupAccessPolicies).toEqual([]); + }); +}); + +describe("convertToServiceAccountGrantedPoliciesView", () => { + it("should convert selected policy values to ServiceAccountGrantedPoliciesView", () => { + const selectedPolicyValues = createProjectApItems(); + + const result = convertToServiceAccountGrantedPoliciesView(selectedPolicyValues); + + expect(result.grantedProjectPolicies).toHaveLength(2); + expect(result.grantedProjectPolicies[0].accessPolicy.grantedProjectId).toBe( + selectedPolicyValues[0].id, + ); + expect(result.grantedProjectPolicies[0].accessPolicy.read).toBe(true); + expect(result.grantedProjectPolicies[0].accessPolicy.write).toBe(false); + + expect(result.grantedProjectPolicies[1].accessPolicy.grantedProjectId).toBe( + selectedPolicyValues[1].id, + ); + expect(result.grantedProjectPolicies[1].accessPolicy.read).toBe(true); + expect(result.grantedProjectPolicies[1].accessPolicy.write).toBe(true); + }); + + it("should return empty array if no selected project policies are provided", () => { + const selectedPolicyValues: ApItemValueType[] = []; + + const result = convertToServiceAccountGrantedPoliciesView(selectedPolicyValues); + + expect(result.grantedProjectPolicies).toEqual([]); + }); +}); + +describe("convertToProjectServiceAccountsAccessPoliciesView", () => { + it("should convert selected policy values to ProjectServiceAccountsAccessPoliciesView", () => { + const selectedPolicyValues = createServiceAccountApItems(); + + const result = convertToProjectServiceAccountsAccessPoliciesView(selectedPolicyValues); + + expect(result.serviceAccountAccessPolicies).toEqual(expectedServiceAccountAccessPolicies); + }); + + it("should return empty array if nothing is selected.", () => { + const selectedPolicyValues: ApItemValueType[] = []; + + const result = convertToProjectServiceAccountsAccessPoliciesView(selectedPolicyValues); + + expect(result.serviceAccountAccessPolicies).toEqual([]); + }); +}); + +describe("convertToSecretAccessPoliciesView", () => { + it("should convert selected policy values to SecretAccessPoliciesView", () => { + const selectedPolicyValues = [ + ...createUserApItems(), + ...createGroupApItems(), + ...createServiceAccountApItems(), + ]; + const result = convertToSecretAccessPoliciesView(selectedPolicyValues); + + expect(result.userAccessPolicies).toEqual(expectedUserAccessPolicies); + expect(result.groupAccessPolicies).toEqual(expectedGroupAccessPolicies); + expect(result.serviceAccountAccessPolicies).toEqual(expectedServiceAccountAccessPolicies); + }); + + it("should return empty user array if no selected users are provided", () => { + const selectedPolicyValues = [...createGroupApItems(), ...createServiceAccountApItems()]; + + const result = convertToSecretAccessPoliciesView(selectedPolicyValues); + + expect(result.userAccessPolicies).toEqual([]); + expect(result.groupAccessPolicies).toEqual(expectedGroupAccessPolicies); + expect(result.serviceAccountAccessPolicies).toEqual(expectedServiceAccountAccessPolicies); + }); + + it("should return empty group array if no selected groups are provided", () => { + const selectedPolicyValues = [...createUserApItems(), ...createServiceAccountApItems()]; + + const result = convertToSecretAccessPoliciesView(selectedPolicyValues); + + expect(result.userAccessPolicies).toEqual(expectedUserAccessPolicies); + expect(result.groupAccessPolicies).toEqual([]); + expect(result.serviceAccountAccessPolicies).toEqual(expectedServiceAccountAccessPolicies); + }); + + it("should return empty service account array if no selected service accounts are provided", () => { + const selectedPolicyValues = [...createUserApItems(), ...createGroupApItems()]; + + const result = convertToSecretAccessPoliciesView(selectedPolicyValues); + + expect(result.userAccessPolicies).toEqual(expectedUserAccessPolicies); + expect(result.groupAccessPolicies).toEqual(expectedGroupAccessPolicies); + expect(result.serviceAccountAccessPolicies).toEqual([]); + }); + + it("should return empty arrays if nothing is selected.", () => { + const selectedPolicyValues: ApItemValueType[] = []; + + const result = convertToSecretAccessPoliciesView(selectedPolicyValues); + + expect(result.userAccessPolicies).toEqual([]); + expect(result.groupAccessPolicies).toEqual([]); + expect(result.serviceAccountAccessPolicies).toEqual([]); + }); +}); + +function createUserApItems(): ApItemValueType[] { + return [ + { + id: "1", + type: ApItemEnum.User, + permission: ApPermissionEnum.CanRead, + }, + { + id: "3", + type: ApItemEnum.User, + permission: ApPermissionEnum.CanReadWrite, + }, + ]; +} + +const expectedUserAccessPolicies = [ + { + organizationUserId: "1", + read: true, + write: false, + }, + { + organizationUserId: "3", + read: true, + write: true, + }, +]; + +function createServiceAccountApItems(): ApItemValueType[] { + return [ + { + id: "1", + type: ApItemEnum.ServiceAccount, + permission: ApPermissionEnum.CanRead, + }, + { + id: "2", + type: ApItemEnum.ServiceAccount, + permission: ApPermissionEnum.CanReadWrite, + }, + ]; +} + +const expectedServiceAccountAccessPolicies = [ + { + serviceAccountId: "1", + read: true, + write: false, + }, + { + serviceAccountId: "2", + read: true, + write: true, + }, +]; + +function createGroupApItems(): ApItemValueType[] { + return [ + { + id: "2", + type: ApItemEnum.Group, + permission: ApPermissionEnum.CanReadWrite, + }, + ]; +} + +const expectedGroupAccessPolicies = [ + { + groupId: "2", + read: true, + write: true, + }, +]; + +function createProjectApItems(): ApItemValueType[] { + return [ + { + id: "1", + type: ApItemEnum.Project, + permission: ApPermissionEnum.CanRead, + }, + { + id: "2", + type: ApItemEnum.Project, + permission: ApPermissionEnum.CanReadWrite, + }, + ]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts index 935c77f1b3b..2de071fb2e8 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts @@ -1,17 +1,15 @@ import { - UserProjectAccessPolicyView, - GroupProjectAccessPolicyView, - UserServiceAccountAccessPolicyView, - GroupServiceAccountAccessPolicyView, - ServiceAccountProjectAccessPolicyView, + UserAccessPolicyView, + GroupAccessPolicyView, + ServiceAccountAccessPolicyView, + GrantedProjectAccessPolicyView, } from "../../../../models/view/access-policies/access-policy.view"; -import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policies/project-people-access-policies.view"; import { ProjectServiceAccountsAccessPoliciesView } from "../../../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { SecretAccessPoliciesView } from "../../../../models/view/access-policies/secret-access-policies.view"; import { ServiceAccountGrantedPoliciesView, - ServiceAccountProjectPolicyPermissionDetailsView, + GrantedProjectPolicyPermissionDetailsView, } from "../../../../models/view/access-policies/service-account-granted-policies.view"; -import { ServiceAccountPeopleAccessPoliciesView } from "../../../../models/view/access-policies/service-account-people-access-policies.view"; import { ApItemEnum } from "./enums/ap-item.enum"; import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum"; @@ -24,67 +22,14 @@ export type ApItemValueType = { currentUser?: boolean; }; -export function convertToProjectPeopleAccessPoliciesView( - projectId: string, - selectedPolicyValues: ApItemValueType[], -): ProjectPeopleAccessPoliciesView { - const view = new ProjectPeopleAccessPoliciesView(); - view.userAccessPolicies = selectedPolicyValues - .filter((x) => x.type == ApItemEnum.User) - .map((filtered) => { - const policyView = new UserProjectAccessPolicyView(); - policyView.grantedProjectId = projectId; - policyView.organizationUserId = filtered.id; - policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); - policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); - return policyView; - }); - - view.groupAccessPolicies = selectedPolicyValues - .filter((x) => x.type == ApItemEnum.Group) - .map((filtered) => { - const policyView = new GroupProjectAccessPolicyView(); - policyView.grantedProjectId = projectId; - policyView.groupId = filtered.id; - policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); - policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); - return policyView; - }); - return view; -} - -export function convertToServiceAccountPeopleAccessPoliciesView( - serviceAccountId: string, - selectedPolicyValues: ApItemValueType[], -): ServiceAccountPeopleAccessPoliciesView { - const view = new ServiceAccountPeopleAccessPoliciesView(); - view.userAccessPolicies = selectedPolicyValues - .filter((x) => x.type == ApItemEnum.User) - .map((filtered) => { - const policyView = new UserServiceAccountAccessPolicyView(); - policyView.grantedServiceAccountId = serviceAccountId; - policyView.organizationUserId = filtered.id; - policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); - policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); - policyView.currentUser = filtered.currentUser; - return policyView; - }); - - view.groupAccessPolicies = selectedPolicyValues - .filter((x) => x.type == ApItemEnum.Group) - .map((filtered) => { - const policyView = new GroupServiceAccountAccessPolicyView(); - policyView.grantedServiceAccountId = serviceAccountId; - policyView.groupId = filtered.id; - policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); - policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); - return policyView; - }); - return view; +export function convertToPeopleAccessPoliciesView(selectedPolicyValues: ApItemValueType[]) { + return { + userAccessPolicies: convertToUserAccessPolicyViews(selectedPolicyValues), + groupAccessPolicies: convertToGroupAccessPolicyViews(selectedPolicyValues), + }; } export function convertToServiceAccountGrantedPoliciesView( - serviceAccountId: string, selectedPolicyValues: ApItemValueType[], ): ServiceAccountGrantedPoliciesView { const view = new ServiceAccountGrantedPoliciesView(); @@ -92,9 +37,8 @@ export function convertToServiceAccountGrantedPoliciesView( view.grantedProjectPolicies = selectedPolicyValues .filter((x) => x.type == ApItemEnum.Project) .map((filtered) => { - const detailView = new ServiceAccountProjectPolicyPermissionDetailsView(); - const policyView = new ServiceAccountProjectAccessPolicyView(); - policyView.serviceAccountId = serviceAccountId; + const detailView = new GrantedProjectPolicyPermissionDetailsView(); + const policyView = new GrantedProjectAccessPolicyView(); policyView.grantedProjectId = filtered.id; policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); @@ -107,21 +51,57 @@ export function convertToServiceAccountGrantedPoliciesView( } export function convertToProjectServiceAccountsAccessPoliciesView( - projectId: string, selectedPolicyValues: ApItemValueType[], ): ProjectServiceAccountsAccessPoliciesView { - const view = new ProjectServiceAccountsAccessPoliciesView(); + return { + serviceAccountAccessPolicies: convertToServiceAccountAccessPolicyViews(selectedPolicyValues), + }; +} - view.serviceAccountAccessPolicies = selectedPolicyValues - .filter((x) => x.type == ApItemEnum.ServiceAccount) +export function convertToSecretAccessPoliciesView( + selectedPolicyValues: ApItemValueType[], +): SecretAccessPoliciesView { + return { + userAccessPolicies: convertToUserAccessPolicyViews(selectedPolicyValues), + groupAccessPolicies: convertToGroupAccessPolicyViews(selectedPolicyValues), + serviceAccountAccessPolicies: convertToServiceAccountAccessPolicyViews(selectedPolicyValues), + }; +} + +function convertToUserAccessPolicyViews(apItemValues: ApItemValueType[]): UserAccessPolicyView[] { + return apItemValues + .filter((x) => x.type == ApItemEnum.User) .map((filtered) => { - const policyView = new ServiceAccountProjectAccessPolicyView(); - policyView.serviceAccountId = filtered.id; - policyView.grantedProjectId = projectId; + const policyView = new UserAccessPolicyView(); + policyView.organizationUserId = filtered.id; + policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); + policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); + return policyView; + }); +} + +function convertToGroupAccessPolicyViews(apItemValues: ApItemValueType[]): GroupAccessPolicyView[] { + return apItemValues + .filter((x) => x.type == ApItemEnum.Group) + .map((filtered) => { + const policyView = new GroupAccessPolicyView(); + policyView.groupId = filtered.id; + policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); + policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); + return policyView; + }); +} + +function convertToServiceAccountAccessPolicyViews( + apItemValues: ApItemValueType[], +): ServiceAccountAccessPolicyView[] { + return apItemValues + .filter((x) => x.type == ApItemEnum.ServiceAccount) + .map((filtered) => { + const policyView = new ServiceAccountAccessPolicyView(); + policyView.serviceAccountId = filtered.id; policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); return policyView; }); - - return view; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.spec.ts new file mode 100644 index 00000000000..c7090391211 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.spec.ts @@ -0,0 +1,310 @@ +import { + GroupAccessPolicyView, + ServiceAccountAccessPolicyView, + UserAccessPolicyView, +} from "../../../../models/view/access-policies/access-policy.view"; +import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policies/project-people-access-policies.view"; +import { ProjectServiceAccountsAccessPoliciesView } from "../../../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { SecretAccessPoliciesView } from "../../../../models/view/access-policies/secret-access-policies.view"; +import { ServiceAccountGrantedPoliciesView } from "../../../../models/view/access-policies/service-account-granted-policies.view"; +import { ServiceAccountPeopleAccessPoliciesView } from "../../../../models/view/access-policies/service-account-people-access-policies.view"; + +import { + convertGrantedPoliciesToAccessPolicyItemViews, + convertProjectServiceAccountsViewToApItemViews, + convertSecretAccessPoliciesToApItemViews, + convertToAccessPolicyItemViews, +} from "./ap-item-view.type"; +import { ApItemEnum } from "./enums/ap-item.enum"; +import { ApPermissionEnum } from "./enums/ap-permission.enum"; + +describe("convertToAccessPolicyItemViews", () => { + it("should convert ProjectPeopleAccessPoliciesView to ApItemViewType array", () => { + const accessPoliciesView: ProjectPeopleAccessPoliciesView = createPeopleAccessPoliciesView(); + + const result = convertToAccessPolicyItemViews(accessPoliciesView); + + expect(result).toEqual([...expectedUserApItemViews, ...expectedGroupApItemViews]); + }); + + it("should convert empty ProjectPeopleAccessPoliciesView to empty ApItemViewType array", () => { + const accessPoliciesView = new ProjectPeopleAccessPoliciesView(); + accessPoliciesView.userAccessPolicies = []; + accessPoliciesView.groupAccessPolicies = []; + + const result = convertToAccessPolicyItemViews(accessPoliciesView); + + expect(result).toEqual([]); + }); + + it("should convert ServiceAccountPeopleAccessPoliciesView to ApItemViewType array", () => { + const accessPoliciesView: ServiceAccountPeopleAccessPoliciesView = + createPeopleAccessPoliciesView(); + + const result = convertToAccessPolicyItemViews(accessPoliciesView); + + expect(result).toEqual([...expectedUserApItemViews, ...expectedGroupApItemViews]); + }); + + it("should convert empty ServiceAccountPeopleAccessPoliciesView to empty ApItemViewType array", () => { + const accessPoliciesView = new ServiceAccountPeopleAccessPoliciesView(); + accessPoliciesView.userAccessPolicies = []; + accessPoliciesView.groupAccessPolicies = []; + + const result = convertToAccessPolicyItemViews(accessPoliciesView); + + expect(result).toEqual([]); + }); +}); + +describe("convertGrantedPoliciesToAccessPolicyItemViews", () => { + it("should convert ServiceAccountGrantedPoliciesView to ApItemViewType array", () => { + const grantedPoliciesView: ServiceAccountGrantedPoliciesView = createGrantedPoliciesView(); + + const result = convertGrantedPoliciesToAccessPolicyItemViews(grantedPoliciesView); + + expect(result).toEqual(expectedGrantedProjectApItemViews); + }); + + it("should convert empty ServiceAccountGrantedPoliciesView to empty ApItemViewType array", () => { + const grantedPoliciesView = new ServiceAccountGrantedPoliciesView(); + grantedPoliciesView.grantedProjectPolicies = []; + + const result = convertGrantedPoliciesToAccessPolicyItemViews(grantedPoliciesView); + + expect(result).toEqual([]); + }); +}); + +describe("convertProjectServiceAccountsViewToApItemViews", () => { + it("should convert ProjectServiceAccountsAccessPoliciesView to ApItemViewType array", () => { + const accessPoliciesView = createProjectServiceAccountsAccessPoliciesView(); + + const result = convertProjectServiceAccountsViewToApItemViews(accessPoliciesView); + + expect(result).toEqual([...expectedServiceAccountAccessPolicyViews]); + }); + + it("should convert empty ProjectPeopleAccessPoliciesView to empty ApItemViewType array", () => { + const accessPoliciesView = new ProjectServiceAccountsAccessPoliciesView(); + accessPoliciesView.serviceAccountAccessPolicies = []; + + const result = convertProjectServiceAccountsViewToApItemViews(accessPoliciesView); + + expect(result).toEqual([]); + }); +}); + +describe("convertSecretAccessPoliciesToApItemViews", () => { + it("should convert SecretAccessPoliciesView to ApItemViewType array", () => { + const accessPoliciesView = createSecretAccessPoliciesView(); + + const result = convertSecretAccessPoliciesToApItemViews(accessPoliciesView); + + expect(result).toEqual([ + ...expectedUserApItemViews, + ...expectedGroupApItemViews, + ...expectedServiceAccountAccessPolicyViews, + ]); + }); + + it("should convert empty SecretAccessPoliciesView to empty ApItemViewType array", () => { + const accessPoliciesView = new SecretAccessPoliciesView(); + accessPoliciesView.userAccessPolicies = []; + accessPoliciesView.groupAccessPolicies = []; + accessPoliciesView.serviceAccountAccessPolicies = []; + + const result = convertSecretAccessPoliciesToApItemViews(accessPoliciesView); + + expect(result).toEqual([]); + }); +}); + +function createUserAccessPolicyViews(): UserAccessPolicyView[] { + return [ + { + organizationUserId: "1", + organizationUserName: "Example organization user name", + read: true, + write: false, + currentUser: true, + }, + { + organizationUserId: "2", + organizationUserName: "Example organization user name", + read: true, + write: true, + currentUser: false, + }, + ]; +} + +const expectedUserApItemViews = [ + { + type: ApItemEnum.User, + icon: "bwi-user", + id: "1", + labelName: "Example organization user name", + listName: "Example organization user name", + permission: ApPermissionEnum.CanRead, + currentUser: true, + readOnly: false, + }, + { + type: ApItemEnum.User, + icon: "bwi-user", + id: "2", + labelName: "Example organization user name", + listName: "Example organization user name", + permission: ApPermissionEnum.CanReadWrite, + currentUser: false, + readOnly: false, + }, +]; + +function createGroupAccessPolicyViews(): GroupAccessPolicyView[] { + return [ + { + groupId: "3", + groupName: "Example group name", + currentUserInGroup: true, + read: true, + write: false, + }, + { + groupId: "4", + groupName: "Example group name", + currentUserInGroup: false, + read: true, + write: true, + }, + ]; +} + +const expectedGroupApItemViews = [ + { + type: ApItemEnum.Group, + icon: "bwi-family", + id: "3", + labelName: "Example group name", + listName: "Example group name", + permission: ApPermissionEnum.CanRead, + currentUserInGroup: true, + readOnly: false, + }, + { + type: ApItemEnum.Group, + icon: "bwi-family", + id: "4", + labelName: "Example group name", + listName: "Example group name", + permission: ApPermissionEnum.CanReadWrite, + currentUserInGroup: false, + readOnly: false, + }, +]; + +function createServiceAccountAccessPolicyViews(): ServiceAccountAccessPolicyView[] { + return [ + { + serviceAccountId: "5", + serviceAccountName: "service account name", + read: true, + write: false, + }, + { + serviceAccountId: "6", + serviceAccountName: "service account name", + read: true, + write: true, + }, + ]; +} + +const expectedServiceAccountAccessPolicyViews = [ + { + type: ApItemEnum.ServiceAccount, + icon: "bwi-wrench", + id: "5", + labelName: "service account name", + listName: "service account name", + permission: ApPermissionEnum.CanRead, + readOnly: false, + }, + { + type: ApItemEnum.ServiceAccount, + icon: "bwi-wrench", + id: "6", + labelName: "service account name", + listName: "service account name", + permission: ApPermissionEnum.CanReadWrite, + readOnly: false, + }, +]; + +function createGrantedPoliciesView() { + return { + grantedProjectPolicies: [ + { + accessPolicy: { + grantedProjectId: "1", + grantedProjectName: "Example project name", + read: true, + write: false, + }, + hasPermission: true, + }, + { + accessPolicy: { + grantedProjectId: "2", + grantedProjectName: "project name", + read: true, + write: true, + }, + hasPermission: false, + }, + ], + }; +} + +const expectedGrantedProjectApItemViews = [ + { + type: ApItemEnum.Project, + icon: "bwi-collection", + id: "1", + labelName: "Example project name", + listName: "Example project name", + permission: ApPermissionEnum.CanRead, + readOnly: false, + }, + { + type: ApItemEnum.Project, + icon: "bwi-collection", + id: "2", + labelName: "project name", + listName: "project name", + permission: ApPermissionEnum.CanReadWrite, + readOnly: true, + }, +]; + +function createPeopleAccessPoliciesView() { + return { + userAccessPolicies: createUserAccessPolicyViews(), + groupAccessPolicies: createGroupAccessPolicyViews(), + }; +} + +function createProjectServiceAccountsAccessPoliciesView(): ProjectServiceAccountsAccessPoliciesView { + return { + serviceAccountAccessPolicies: createServiceAccountAccessPolicyViews(), + }; +} + +function createSecretAccessPoliciesView(): SecretAccessPoliciesView { + return { + userAccessPolicies: createUserAccessPolicyViews(), + groupAccessPolicies: createGroupAccessPolicyViews(), + serviceAccountAccessPolicies: createServiceAccountAccessPolicyViews(), + }; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts index 1a023659c16..52a91c5fd6d 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts @@ -1,9 +1,15 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SelectItemView } from "@bitwarden/components"; +import { + GroupAccessPolicyView, + ServiceAccountAccessPolicyView, + UserAccessPolicyView, +} from "../../../../models/view/access-policies/access-policy.view"; import { PotentialGranteeView } from "../../../../models/view/access-policies/potential-grantee.view"; import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policies/project-people-access-policies.view"; import { ProjectServiceAccountsAccessPoliciesView } from "../../../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { SecretAccessPoliciesView } from "../../../../models/view/access-policies/secret-access-policies.view"; import { ServiceAccountGrantedPoliciesView } from "../../../../models/view/access-policies/service-account-granted-policies.view"; import { ServiceAccountPeopleAccessPoliciesView } from "../../../../models/view/access-policies/service-account-people-access-policies.view"; @@ -11,7 +17,6 @@ import { ApItemEnum, ApItemEnumUtil } from "./enums/ap-item.enum"; import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum"; export type ApItemViewType = SelectItemView & { - accessPolicyId?: string; permission?: ApPermissionEnum; /** * Flag that this item cannot be modified. @@ -22,7 +27,6 @@ export type ApItemViewType = SelectItemView & { } & ( | { type: ApItemEnum.User; - userId?: string; currentUser?: boolean; } | { @@ -40,38 +44,10 @@ export type ApItemViewType = SelectItemView & { export function convertToAccessPolicyItemViews( value: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView, ): ApItemViewType[] { - const accessPolicies: ApItemViewType[] = []; - - value.userAccessPolicies.forEach((policy) => { - accessPolicies.push({ - type: ApItemEnum.User, - icon: ApItemEnumUtil.itemIcon(ApItemEnum.User), - id: policy.organizationUserId, - accessPolicyId: policy.id, - labelName: policy.organizationUserName, - listName: policy.organizationUserName, - permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), - userId: policy.userId, - currentUser: policy.currentUser, - readOnly: false, - }); - }); - - value.groupAccessPolicies.forEach((policy) => { - accessPolicies.push({ - type: ApItemEnum.Group, - icon: ApItemEnumUtil.itemIcon(ApItemEnum.Group), - id: policy.groupId, - accessPolicyId: policy.id, - labelName: policy.groupName, - listName: policy.groupName, - permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), - currentUserInGroup: policy.currentUserInGroup, - readOnly: false, - }); - }); - - return accessPolicies; + return [ + ...toUserApItemViews(value.userAccessPolicies), + ...toGroupApItemViews(value.groupAccessPolicies), + ]; } export function convertGrantedPoliciesToAccessPolicyItemViews( @@ -84,7 +60,6 @@ export function convertGrantedPoliciesToAccessPolicyItemViews( type: ApItemEnum.Project, icon: ApItemEnumUtil.itemIcon(ApItemEnum.Project), id: detailView.accessPolicy.grantedProjectId, - accessPolicyId: detailView.accessPolicy.id, labelName: detailView.accessPolicy.grantedProjectName, listName: detailView.accessPolicy.grantedProjectName, permission: ApPermissionEnumUtil.toApPermissionEnum( @@ -100,24 +75,17 @@ export function convertGrantedPoliciesToAccessPolicyItemViews( export function convertProjectServiceAccountsViewToApItemViews( value: ProjectServiceAccountsAccessPoliciesView, ): ApItemViewType[] { - const accessPolicies: ApItemViewType[] = []; + return toServiceAccountsApItemViews(value.serviceAccountAccessPolicies); +} - value.serviceAccountAccessPolicies.forEach((accessPolicyView) => { - accessPolicies.push({ - type: ApItemEnum.ServiceAccount, - icon: ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount), - id: accessPolicyView.serviceAccountId, - accessPolicyId: accessPolicyView.id, - labelName: accessPolicyView.serviceAccountName, - listName: accessPolicyView.serviceAccountName, - permission: ApPermissionEnumUtil.toApPermissionEnum( - accessPolicyView.read, - accessPolicyView.write, - ), - readOnly: false, - }); - }); - return accessPolicies; +export function convertSecretAccessPoliciesToApItemViews( + value: SecretAccessPoliciesView, +): ApItemViewType[] { + return [ + ...toUserApItemViews(value.userAccessPolicies), + ...toGroupApItemViews(value.groupAccessPolicies), + ...toServiceAccountsApItemViews(value.serviceAccountAccessPolicies), + ]; } export function convertPotentialGranteesToApItemViewType( @@ -166,3 +134,49 @@ export function convertPotentialGranteesToApItemViewType( }; }); } + +function toUserApItemViews(policies: UserAccessPolicyView[]): ApItemViewType[] { + return policies.map((policy) => { + return { + type: ApItemEnum.User, + icon: ApItemEnumUtil.itemIcon(ApItemEnum.User), + id: policy.organizationUserId, + labelName: policy.organizationUserName, + listName: policy.organizationUserName, + permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), + currentUser: policy.currentUser, + readOnly: false, + }; + }); +} + +function toGroupApItemViews(policies: GroupAccessPolicyView[]): ApItemViewType[] { + return policies.map((policy) => { + return { + type: ApItemEnum.Group, + icon: ApItemEnumUtil.itemIcon(ApItemEnum.Group), + id: policy.groupId, + labelName: policy.groupName, + listName: policy.groupName, + permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), + currentUserInGroup: policy.currentUserInGroup, + readOnly: false, + }; + }); +} + +function toServiceAccountsApItemViews( + policies: ServiceAccountAccessPolicyView[], +): ApItemViewType[] { + return policies.map((policy) => { + return { + type: ApItemEnum.ServiceAccount, + icon: ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount), + id: policy.serviceAccountId, + labelName: policy.serviceAccountName, + listName: policy.serviceAccountName, + permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), + readOnly: false, + }; + }); +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts index 32c130647a1..67fb9b19bca 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts @@ -8,18 +8,18 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { - UserProjectAccessPolicyView, - GroupProjectAccessPolicyView, - UserServiceAccountAccessPolicyView, - GroupServiceAccountAccessPolicyView, - ServiceAccountProjectAccessPolicyView, + UserAccessPolicyView, + GroupAccessPolicyView, + ServiceAccountAccessPolicyView, + GrantedProjectAccessPolicyView, } from "../../models/view/access-policies/access-policy.view"; import { PotentialGranteeView } from "../../models/view/access-policies/potential-grantee.view"; import { ProjectPeopleAccessPoliciesView } from "../../models/view/access-policies/project-people-access-policies.view"; import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { SecretAccessPoliciesView } from "../../models/view/access-policies/secret-access-policies.view"; import { ServiceAccountGrantedPoliciesView, - ServiceAccountProjectPolicyPermissionDetailsView, + GrantedProjectPolicyPermissionDetailsView, } from "../../models/view/access-policies/service-account-granted-policies.view"; import { ServiceAccountPeopleAccessPoliciesView } from "../../models/view/access-policies/service-account-people-access-policies.view"; import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request"; @@ -28,18 +28,18 @@ import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/ import { AccessPolicyRequest } from "./models/requests/access-policy.request"; import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request"; import { - GroupServiceAccountAccessPolicyResponse, - UserServiceAccountAccessPolicyResponse, - GroupProjectAccessPolicyResponse, - ServiceAccountProjectAccessPolicyResponse, - UserProjectAccessPolicyResponse, + GroupAccessPolicyResponse, + UserAccessPolicyResponse, + ServiceAccountAccessPolicyResponse, + GrantedProjectAccessPolicyResponse, } from "./models/responses/access-policy.response"; import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response"; import { ProjectServiceAccountsAccessPoliciesResponse } from "./models/responses/project-service-accounts-access-policies.response"; +import { SecretAccessPoliciesResponse } from "./models/responses/secret-access-policies.response"; import { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response"; import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response"; -import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response"; +import { GrantedProjectAccessPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response"; @Injectable({ providedIn: "root", @@ -63,7 +63,7 @@ export class AccessPolicyService { ); const results = new ProjectPeopleAccessPoliciesResponse(r); - return this.createProjectPeopleAccessPoliciesView(results); + return this.createPeopleAccessPoliciesView(results); } async putProjectPeopleAccessPolicies( @@ -79,7 +79,7 @@ export class AccessPolicyService { true, ); const results = new ProjectPeopleAccessPoliciesResponse(r); - return this.createProjectPeopleAccessPoliciesView(results); + return this.createPeopleAccessPoliciesView(results); } async getServiceAccountPeopleAccessPolicies( @@ -94,7 +94,7 @@ export class AccessPolicyService { ); const results = new ServiceAccountPeopleAccessPoliciesResponse(r); - return this.createServiceAccountPeopleAccessPoliciesView(results); + return this.createPeopleAccessPoliciesView(results); } async putServiceAccountPeopleAccessPolicies( @@ -110,7 +110,7 @@ export class AccessPolicyService { true, ); const results = new ServiceAccountPeopleAccessPoliciesResponse(r); - return this.createServiceAccountPeopleAccessPoliciesView(results); + return this.createPeopleAccessPoliciesView(results); } async getServiceAccountGrantedPolicies( @@ -181,6 +181,22 @@ export class AccessPolicyService { return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId); } + async getSecretAccessPolicies( + organizationId: string, + secretId: string, + ): Promise { + const r = await this.apiService.send( + "GET", + "/secrets/" + secretId + "/access-policies", + null, + true, + true, + ); + + const result = new SecretAccessPoliciesResponse(r); + return await this.createSecretAccessPoliciesView(result, organizationId); + } + async getPeoplePotentialGrantees(organizationId: string) { const r = await this.apiService.send( "GET", @@ -223,12 +239,7 @@ export class AccessPolicyService { private getAccessPolicyRequest( granteeId: string, - view: - | UserProjectAccessPolicyView - | UserServiceAccountAccessPolicyView - | GroupProjectAccessPolicyView - | GroupServiceAccountAccessPolicyView - | ServiceAccountProjectAccessPolicyView, + view: UserAccessPolicyView | GroupAccessPolicyView | ServiceAccountAccessPolicyView, ) { const request = new AccessPolicyRequest(); request.granteeId = granteeId; @@ -285,21 +296,79 @@ export class AccessPolicyService { private createBaseAccessPolicyView( response: - | UserProjectAccessPolicyResponse - | UserServiceAccountAccessPolicyResponse - | GroupProjectAccessPolicyResponse - | GroupServiceAccountAccessPolicyResponse - | ServiceAccountProjectAccessPolicyResponse, + | UserAccessPolicyResponse + | GroupAccessPolicyResponse + | ServiceAccountAccessPolicyResponse + | GrantedProjectAccessPolicyResponse, ) { return { - id: response.id, read: response.read, write: response.write, - creationDate: response.creationDate, - revisionDate: response.revisionDate, }; } + private async createGrantedProjectAccessPolicyView( + organizationKey: SymmetricCryptoKey, + response: GrantedProjectAccessPolicyResponse, + ): Promise { + return { + ...this.createBaseAccessPolicyView(response), + grantedProjectId: response.grantedProjectId, + grantedProjectName: response.grantedProjectName + ? await this.encryptService.decryptToUtf8( + new EncString(response.grantedProjectName), + organizationKey, + ) + : null, + }; + } + + private createUserAccessPolicyViews( + responses: UserAccessPolicyResponse[], + ): UserAccessPolicyView[] { + return responses.map((response) => { + return { + ...this.createBaseAccessPolicyView(response), + organizationUserId: response.organizationUserId, + organizationUserName: response.organizationUserName, + currentUser: response.currentUser, + }; + }); + } + + private createGroupAccessPolicyViews( + responses: GroupAccessPolicyResponse[], + ): GroupAccessPolicyView[] { + return responses.map((response) => { + return { + ...this.createBaseAccessPolicyView(response), + groupId: response.groupId, + groupName: response.groupName, + currentUserInGroup: response.currentUserInGroup, + }; + }); + } + + private async createServiceAccountAccessPolicyViews( + orgKey: SymmetricCryptoKey, + responses: ServiceAccountAccessPolicyResponse[], + ): Promise { + return await Promise.all( + responses.map(async (response) => { + return { + ...this.createBaseAccessPolicyView(response), + serviceAccountId: response.serviceAccountId, + serviceAccountName: response.serviceAccountName + ? await this.encryptService.decryptToUtf8( + new EncString(response.serviceAccountName), + orgKey, + ) + : null, + }; + }), + ); + } + private async createPotentialGranteeViews( organizationId: string, results: PotentialGranteeResponse[], @@ -332,137 +401,44 @@ export class AccessPolicyService { ): Promise { const orgKey = await this.getOrganizationKey(organizationId); - const view = new ServiceAccountGrantedPoliciesView(); - view.grantedProjectPolicies = - await this.createServiceAccountProjectPolicyPermissionDetailsViews( + return { + grantedProjectPolicies: await this.createGrantedProjectPolicyPermissionDetailsViews( orgKey, response.grantedProjectPolicies, - ); - return view; + ), + }; } - private async createServiceAccountProjectPolicyPermissionDetailsViews( + private async createGrantedProjectPolicyPermissionDetailsViews( orgKey: SymmetricCryptoKey, - responses: ServiceAccountProjectPolicyPermissionDetailsResponse[], - ): Promise { + responses: GrantedProjectAccessPolicyPermissionDetailsResponse[], + ): Promise { return await Promise.all( responses.map(async (response) => { - return await this.createServiceAccountProjectPolicyPermissionDetailsView(orgKey, response); + return await this.createGrantedProjectPolicyPermissionDetailsView(orgKey, response); }), ); } - private async createServiceAccountProjectPolicyPermissionDetailsView( + private async createGrantedProjectPolicyPermissionDetailsView( orgKey: SymmetricCryptoKey, - response: ServiceAccountProjectPolicyPermissionDetailsResponse, - ): Promise { - const view = new ServiceAccountProjectPolicyPermissionDetailsView(); + response: GrantedProjectAccessPolicyPermissionDetailsResponse, + ): Promise { + const view = new GrantedProjectPolicyPermissionDetailsView(); view.hasPermission = response.hasPermission; - view.accessPolicy = await this.createServiceAccountProjectAccessPolicyView( + view.accessPolicy = await this.createGrantedProjectAccessPolicyView( orgKey, response.accessPolicy, ); return view; } - private createProjectPeopleAccessPoliciesView( - peopleAccessPoliciesResponse: ProjectPeopleAccessPoliciesResponse, - ): ProjectPeopleAccessPoliciesView { - const view = new ProjectPeopleAccessPoliciesView(); - - view.userAccessPolicies = peopleAccessPoliciesResponse.userAccessPolicies.map((ap) => { - return this.createUserProjectAccessPolicyView(ap); - }); - view.groupAccessPolicies = peopleAccessPoliciesResponse.groupAccessPolicies.map((ap) => { - return this.createGroupProjectAccessPolicyView(ap); - }); - return view; - } - - private createServiceAccountPeopleAccessPoliciesView( - response: ServiceAccountPeopleAccessPoliciesResponse, - ): ServiceAccountPeopleAccessPoliciesView { - const view = new ServiceAccountPeopleAccessPoliciesView(); - - view.userAccessPolicies = response.userAccessPolicies.map((ap) => { - return this.createUserServiceAccountAccessPolicyView(ap); - }); - view.groupAccessPolicies = response.groupAccessPolicies.map((ap) => { - return this.createGroupServiceAccountAccessPolicyView(ap); - }); - return view; - } - - private createUserProjectAccessPolicyView( - response: UserProjectAccessPolicyResponse, - ): UserProjectAccessPolicyView { + private createPeopleAccessPoliciesView( + response: ProjectPeopleAccessPoliciesResponse | ServiceAccountPeopleAccessPoliciesResponse, + ) { return { - ...this.createBaseAccessPolicyView(response), - grantedProjectId: response.grantedProjectId, - organizationUserId: response.organizationUserId, - organizationUserName: response.organizationUserName, - userId: response.userId, - currentUser: response.currentUser, - }; - } - - private createGroupProjectAccessPolicyView( - response: GroupProjectAccessPolicyResponse, - ): GroupProjectAccessPolicyView { - return { - ...this.createBaseAccessPolicyView(response), - grantedProjectId: response.grantedProjectId, - groupId: response.groupId, - groupName: response.groupName, - currentUserInGroup: response.currentUserInGroup, - }; - } - - private async createServiceAccountProjectAccessPolicyView( - organizationKey: SymmetricCryptoKey, - response: ServiceAccountProjectAccessPolicyResponse, - ): Promise { - return { - ...this.createBaseAccessPolicyView(response), - grantedProjectId: response.grantedProjectId, - serviceAccountId: response.serviceAccountId, - grantedProjectName: response.grantedProjectName - ? await this.encryptService.decryptToUtf8( - new EncString(response.grantedProjectName), - organizationKey, - ) - : null, - serviceAccountName: response.serviceAccountName - ? await this.encryptService.decryptToUtf8( - new EncString(response.serviceAccountName), - organizationKey, - ) - : null, - }; - } - - private createUserServiceAccountAccessPolicyView( - response: UserServiceAccountAccessPolicyResponse, - ): UserServiceAccountAccessPolicyView { - return { - ...this.createBaseAccessPolicyView(response), - grantedServiceAccountId: response.grantedServiceAccountId, - organizationUserId: response.organizationUserId, - organizationUserName: response.organizationUserName, - userId: response.userId, - currentUser: response.currentUser, - }; - } - - private createGroupServiceAccountAccessPolicyView( - response: GroupServiceAccountAccessPolicyResponse, - ): GroupServiceAccountAccessPolicyView { - return { - ...this.createBaseAccessPolicyView(response), - grantedServiceAccountId: response.grantedServiceAccountId, - groupId: response.groupId, - groupName: response.groupName, - currentUserInGroup: response.currentUserInGroup, + userAccessPolicies: this.createUserAccessPolicyViews(response.userAccessPolicies), + groupAccessPolicies: this.createGroupAccessPolicyViews(response.groupAccessPolicies), }; } @@ -471,13 +447,26 @@ export class AccessPolicyService { organizationId: string, ): Promise { const orgKey = await this.getOrganizationKey(organizationId); + return { + serviceAccountAccessPolicies: await this.createServiceAccountAccessPolicyViews( + orgKey, + response.serviceAccountAccessPolicies, + ), + }; + } - const view = new ProjectServiceAccountsAccessPoliciesView(); - view.serviceAccountAccessPolicies = await Promise.all( - response.serviceAccountAccessPolicies.map(async (ap) => { - return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap); - }), - ); - return view; + private async createSecretAccessPoliciesView( + response: SecretAccessPoliciesResponse, + organizationId: string, + ): Promise { + const orgKey = await this.getOrganizationKey(organizationId); + return { + userAccessPolicies: this.createUserAccessPolicyViews(response.userAccessPolicies), + groupAccessPolicies: this.createGroupAccessPolicyViews(response.groupAccessPolicies), + serviceAccountAccessPolicies: await this.createServiceAccountAccessPolicyViews( + orgKey, + response.serviceAccountAccessPolicies, + ), + }; } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts index ef076f9b592..88399b4e121 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts @@ -1,96 +1,59 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; -export class BaseAccessPolicyResponse extends BaseResponse { - id: string; +class BaseAccessPolicyResponse extends BaseResponse { read: boolean; write: boolean; - creationDate: string; - revisionDate: string; constructor(response: any) { super(response); - this.id = this.getResponseProperty("Id"); this.read = this.getResponseProperty("Read"); this.write = this.getResponseProperty("Write"); - this.creationDate = this.getResponseProperty("CreationDate"); - this.revisionDate = this.getResponseProperty("RevisionDate"); } } -export class UserProjectAccessPolicyResponse extends BaseAccessPolicyResponse { +export class UserAccessPolicyResponse extends BaseAccessPolicyResponse { organizationUserId: string; organizationUserName: string; - grantedProjectId: string; - userId: string; currentUser: boolean; constructor(response: any) { super(response); this.organizationUserId = this.getResponseProperty("OrganizationUserId"); this.organizationUserName = this.getResponseProperty("OrganizationUserName"); - this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); - this.userId = this.getResponseProperty("UserId"); this.currentUser = this.getResponseProperty("CurrentUser"); } } -export class UserServiceAccountAccessPolicyResponse extends BaseAccessPolicyResponse { - organizationUserId: string; - organizationUserName: string; - grantedServiceAccountId: string; - userId: string; - currentUser: boolean; - - constructor(response: any) { - super(response); - this.organizationUserId = this.getResponseProperty("OrganizationUserId"); - this.organizationUserName = this.getResponseProperty("OrganizationUserName"); - this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId"); - this.userId = this.getResponseProperty("UserId"); - this.currentUser = this.getResponseProperty("CurrentUser"); - } -} - -export class GroupProjectAccessPolicyResponse extends BaseAccessPolicyResponse { +export class GroupAccessPolicyResponse extends BaseAccessPolicyResponse { groupId: string; groupName: string; - grantedProjectId: string; currentUserInGroup: boolean; constructor(response: any) { super(response); this.groupId = this.getResponseProperty("GroupId"); this.groupName = this.getResponseProperty("GroupName"); - this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup"); } } -export class GroupServiceAccountAccessPolicyResponse extends BaseAccessPolicyResponse { - groupId: string; - groupName: string; - grantedServiceAccountId: string; - currentUserInGroup: boolean; - - constructor(response: any) { - super(response); - this.groupId = this.getResponseProperty("GroupId"); - this.groupName = this.getResponseProperty("GroupName"); - this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId"); - this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup"); - } -} - -export class ServiceAccountProjectAccessPolicyResponse extends BaseAccessPolicyResponse { +export class ServiceAccountAccessPolicyResponse extends BaseAccessPolicyResponse { serviceAccountId: string; serviceAccountName: string; - grantedProjectId: string; - grantedProjectName: string; constructor(response: any) { super(response); this.serviceAccountId = this.getResponseProperty("ServiceAccountId"); this.serviceAccountName = this.getResponseProperty("ServiceAccountName"); + } +} + +export class GrantedProjectAccessPolicyResponse extends BaseAccessPolicyResponse { + grantedProjectId: string; + grantedProjectName: string; + + constructor(response: any) { + super(response); this.grantedProjectId = this.getResponseProperty("GrantedProjectId"); this.grantedProjectName = this.getResponseProperty("GrantedProjectName"); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-people-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-people-access-policies.response.ts index 3fa0fa652e0..fbd27168074 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-people-access-policies.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-people-access-policies.response.ts @@ -1,23 +1,18 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; -import { - GroupProjectAccessPolicyResponse, - UserProjectAccessPolicyResponse, -} from "./access-policy.response"; +import { GroupAccessPolicyResponse, UserAccessPolicyResponse } from "./access-policy.response"; export class ProjectPeopleAccessPoliciesResponse extends BaseResponse { - userAccessPolicies: UserProjectAccessPolicyResponse[]; - groupAccessPolicies: GroupProjectAccessPolicyResponse[]; + userAccessPolicies: UserAccessPolicyResponse[]; + groupAccessPolicies: GroupAccessPolicyResponse[]; constructor(response: any) { super(response); const userAccessPolicies = this.getResponseProperty("UserAccessPolicies"); - this.userAccessPolicies = userAccessPolicies.map( - (k: any) => new UserProjectAccessPolicyResponse(k), - ); + this.userAccessPolicies = userAccessPolicies.map((k: any) => new UserAccessPolicyResponse(k)); const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies"); this.groupAccessPolicies = groupAccessPolicies.map( - (k: any) => new GroupProjectAccessPolicyResponse(k), + (k: any) => new GroupAccessPolicyResponse(k), ); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts index f26a9996ddc..f50be0ca9a7 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts @@ -1,15 +1,15 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; -import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response"; +import { ServiceAccountAccessPolicyResponse } from "./access-policy.response"; export class ProjectServiceAccountsAccessPoliciesResponse extends BaseResponse { - serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[]; + serviceAccountAccessPolicies: ServiceAccountAccessPolicyResponse[]; constructor(response: any) { super(response); const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies"); this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map( - (k: any) => new ServiceAccountProjectAccessPolicyResponse(k), + (k: any) => new ServiceAccountAccessPolicyResponse(k), ); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/secret-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/secret-access-policies.response.ts new file mode 100644 index 00000000000..ccfcb02d1d3 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/secret-access-policies.response.ts @@ -0,0 +1,27 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +import { + GroupAccessPolicyResponse, + UserAccessPolicyResponse, + ServiceAccountAccessPolicyResponse, +} from "./access-policy.response"; + +export class SecretAccessPoliciesResponse extends BaseResponse { + userAccessPolicies: UserAccessPolicyResponse[]; + groupAccessPolicies: GroupAccessPolicyResponse[]; + serviceAccountAccessPolicies: ServiceAccountAccessPolicyResponse[]; + + constructor(response: any) { + super(response); + const userAccessPolicies = this.getResponseProperty("UserAccessPolicies"); + this.userAccessPolicies = userAccessPolicies.map((k: any) => new UserAccessPolicyResponse(k)); + const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies"); + this.groupAccessPolicies = groupAccessPolicies.map( + (k: any) => new GroupAccessPolicyResponse(k), + ); + const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies"); + this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map( + (k: any) => new ServiceAccountAccessPolicyResponse(k), + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts index 858a59ff433..8925e92f883 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts @@ -1,15 +1,15 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; -import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./service-account-project-policy-permission-details.response"; +import { GrantedProjectAccessPolicyPermissionDetailsResponse } from "./service-account-project-policy-permission-details.response"; export class ServiceAccountGrantedPoliciesPermissionDetailsResponse extends BaseResponse { - grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsResponse[]; + grantedProjectPolicies: GrantedProjectAccessPolicyPermissionDetailsResponse[]; constructor(response: any) { super(response); const grantedProjectPolicies = this.getResponseProperty("GrantedProjectPolicies"); this.grantedProjectPolicies = grantedProjectPolicies.map( - (k: any) => new ServiceAccountProjectPolicyPermissionDetailsResponse(k), + (k: any) => new GrantedProjectAccessPolicyPermissionDetailsResponse(k), ); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-people-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-people-access-policies.response.ts index ca134d9012a..74796a47890 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-people-access-policies.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-people-access-policies.response.ts @@ -1,23 +1,18 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; -import { - GroupServiceAccountAccessPolicyResponse, - UserServiceAccountAccessPolicyResponse, -} from "./access-policy.response"; +import { GroupAccessPolicyResponse, UserAccessPolicyResponse } from "./access-policy.response"; export class ServiceAccountPeopleAccessPoliciesResponse extends BaseResponse { - userAccessPolicies: UserServiceAccountAccessPolicyResponse[]; - groupAccessPolicies: GroupServiceAccountAccessPolicyResponse[]; + userAccessPolicies: UserAccessPolicyResponse[]; + groupAccessPolicies: GroupAccessPolicyResponse[]; constructor(response: any) { super(response); const userAccessPolicies = this.getResponseProperty("UserAccessPolicies"); - this.userAccessPolicies = userAccessPolicies.map( - (k: any) => new UserServiceAccountAccessPolicyResponse(k), - ); + this.userAccessPolicies = userAccessPolicies.map((k: any) => new UserAccessPolicyResponse(k)); const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies"); this.groupAccessPolicies = groupAccessPolicies.map( - (k: any) => new GroupServiceAccountAccessPolicyResponse(k), + (k: any) => new GroupAccessPolicyResponse(k), ); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts index dbc4fe0727d..9cbc6efed8f 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts @@ -1,9 +1,9 @@ import { BaseResponse } from "@bitwarden/common/models/response/base.response"; -import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response"; +import { GrantedProjectAccessPolicyResponse } from "./access-policy.response"; -export class ServiceAccountProjectPolicyPermissionDetailsResponse extends BaseResponse { - accessPolicy: ServiceAccountProjectAccessPolicyResponse; +export class GrantedProjectAccessPolicyPermissionDetailsResponse extends BaseResponse { + accessPolicy: GrantedProjectAccessPolicyResponse; hasPermission: boolean; constructor(response: any) { 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 @@