1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

Merge branch 'main' into autofill/pm-8027-inline-menu-appears-within-input-fields-that-do-not-relate-to-user-login

This commit is contained in:
Cesar Gonzalez
2024-06-11 08:35:38 -05:00
committed by GitHub
103 changed files with 1678 additions and 1027 deletions

View File

@@ -112,13 +112,48 @@ jobs:
echo "azure-login-creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT echo "azure-login-creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT
echo "retrieve-secrets-keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT echo "retrieve-secrets-keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT
echo "environment-artifact=web-*-cloud-usdev.zip" >> $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 echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT
;; ;;
esac esac
# Set the sync utility to use for deployment to the environment (az-sync or azcopy) # Set the sync utility to use for deployment to the environment (az-sync or azcopy)
echo "sync-utility=azcopy" >> $GITHUB_OUTPUT 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: approval:
name: Approval for Deployment to ${{ needs.setup.outputs.environment-name }} name: Approval for Deployment to ${{ needs.setup.outputs.environment-name }}
needs: setup needs: setup
@@ -206,6 +241,31 @@ jobs:
echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT
fi 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: notify-start:
name: Notify Slack with start message name: Notify Slack with start message
needs: needs:

View File

@@ -8,16 +8,27 @@ on:
- "main" - "main"
- "rc" - "rc"
- "hotfix-rc-*" - "hotfix-rc-*"
pull_request: pull_request_target:
types: [opened, synchronize]
defaults: defaults:
run: run:
shell: bash shell: bash
jobs: jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
test: test:
name: Run tests name: Run tests
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: check-run
permissions:
checks: write
contents: read
pull-requests: write
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

View File

@@ -1434,6 +1434,24 @@
"typeIdentity": { "typeIdentity": {
"message": "Identity" "message": "Identity"
}, },
"newItemHeader":{
"message": "New $TYPE$",
"placeholders": {
"type": {
"content": "$1",
"example": "Login"
}
}
},
"editItemHeader":{
"message": "Edit $TYPE$",
"placeholders": {
"type": {
"content": "$1",
"example": "Login"
}
}
},
"passwordHistory": { "passwordHistory": {
"message": "Password history" "message": "Password history"
}, },

View File

@@ -57,6 +57,7 @@ import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filt
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
import { ViewComponent } from "../vault/popup/components/vault/view.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 { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component";
@@ -195,20 +196,18 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
data: { state: "cipher-password-history" }, data: { state: "cipher-password-history" },
}, },
{ ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
path: "add-cipher", path: "add-cipher",
component: AddEditComponent,
canActivate: [AuthGuard, debounceNavigationGuard()], canActivate: [AuthGuard, debounceNavigationGuard()],
data: { state: "add-cipher" }, data: { state: "add-cipher" },
runGuardsAndResolvers: "always", runGuardsAndResolvers: "always",
}, }),
{ ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
path: "edit-cipher", path: "edit-cipher",
component: AddEditComponent,
canActivate: [AuthGuard, debounceNavigationGuard()], canActivate: [AuthGuard, debounceNavigationGuard()],
data: { state: "edit-cipher" }, data: { state: "edit-cipher" },
runGuardsAndResolvers: "always", runGuardsAndResolvers: "always",
}, }),
{ {
path: "share-cipher", path: "share-cipher",
component: ShareComponent, component: ShareComponent,

View File

@@ -0,0 +1,9 @@
<popup-page>
<popup-header slot="header" [pageTitle]="headerText" showBackButton> </popup-header>
<popup-footer slot="footer">
<button bitButton type="button" buttonType="primary">
{{ "save" | i18n }}
</button>
</popup-footer>
</popup-page>

View File

@@ -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"));
}
}
}

View File

@@ -0,0 +1,22 @@
<button bitButton [bitMenuTriggerFor]="itemOptions" buttonType="primary" type="button">
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}
</button>
<bit-menu #itemOptions>
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</a>
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</a>
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</a>
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</a>
</bit-menu>

View File

@@ -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 } });
}
}

View File

@@ -1,11 +1,8 @@
<popup-page> <popup-page>
<popup-header slot="header" [pageTitle]="'vault' | i18n"> <popup-header slot="header" [pageTitle]="'vault' | i18n">
<ng-container slot="end"> <ng-container slot="end">
<!-- TODO PM-6826: add selectedVault query param --> <app-new-item-dropdown></app-new-item-dropdown>
<a bitButton buttonType="primary" type="button" routerLink="/add-cipher">
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}
</a>
<app-pop-out></app-pop-out> <app-pop-out></app-pop-out>
<app-current-account></app-current-account> <app-current-account></app-current-account>
</ng-container> </ng-container>
@@ -18,9 +15,7 @@
<bit-no-items [icon]="vaultIcon"> <bit-no-items [icon]="vaultIcon">
<ng-container slot="title">{{ "yourVaultIsEmpty" | i18n }}</ng-container> <ng-container slot="title">{{ "yourVaultIsEmpty" | i18n }}</ng-container>
<ng-container slot="description">{{ "autofillSuggestionsTip" | i18n }}</ng-container> <ng-container slot="description">{{ "autofillSuggestionsTip" | i18n }}</ng-container>
<button slot="button" type="button" bitButton buttonType="primary" (click)="addCipher()"> <app-new-item-dropdown slot="button"></app-new-item-dropdown>
{{ "new" | i18n }}
</button>
</bit-no-items> </bit-no-items>
</div> </div>

View File

@@ -5,6 +5,7 @@ import { Router, RouterLink } from "@angular/router";
import { combineLatest } from "rxjs"; import { combineLatest } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components"; import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component"; 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 { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2"; 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 { VaultListFiltersComponent } from "../vault-v2/vault-list-filters/vault-list-filters.component";
import { VaultV2SearchComponent } from "../vault-v2/vault-search/vault-v2-search.component"; import { VaultV2SearchComponent } from "../vault-v2/vault-search/vault-v2-search.component";
@@ -40,9 +42,11 @@ enum VaultState {
ButtonModule, ButtonModule,
RouterLink, RouterLink,
VaultV2SearchComponent, VaultV2SearchComponent,
NewItemDropdownV2Component,
], ],
}) })
export class VaultV2Component implements OnInit, OnDestroy { export class VaultV2Component implements OnInit, OnDestroy {
cipherType = CipherType;
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$; protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$; protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
@@ -86,9 +90,4 @@ export class VaultV2Component implements OnInit, OnDestroy {
ngOnInit(): void {} ngOnInit(): void {}
ngOnDestroy(): void {} ngOnDestroy(): void {}
addCipher() {
// TODO: Add currently filtered organization to query params if available
void this.router.navigate(["/add-cipher"], {});
}
} }

View File

@@ -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. // 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 // 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; return true;
} }

View File

@@ -379,6 +379,54 @@ describe("VaultPopupItemsService", () => {
}); });
}); });
describe("loading$", () => {
let tracked: ObservableTracker<boolean>;
let trackedCiphers: ObservableTracker<any>;
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<any>).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", () => { describe("applyFilter", () => {
it("should call search Service with the new search term", (done) => { it("should call search Service with the new search term", (done) => {
const searchText = "Hello"; const searchText = "Hello";

View File

@@ -2,6 +2,7 @@ import { inject, Injectable, NgZone } from "@angular/core";
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
distinctUntilChanged,
distinctUntilKeyChanged, distinctUntilKeyChanged,
from, from,
map, map,
@@ -12,6 +13,8 @@ import {
startWith, startWith,
Subject, Subject,
switchMap, switchMap,
tap,
withLatestFrom,
} from "rxjs"; } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; 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 { export class VaultPopupItemsService {
private _refreshCurrentTab$ = new Subject<void>(); private _refreshCurrentTab$ = new Subject<void>();
private _searchText$ = new BehaviorSubject<string>(""); private _searchText$ = new BehaviorSubject<string>("");
/**
* Subject that emits whenever new ciphers are being processed/filtered.
* @private
*/
private _ciphersLoading$ = new Subject<void>();
latestSearchText$: Observable<string> = this._searchText$.asObservable(); latestSearchText$: Observable<string> = this._searchText$.asObservable();
/** /**
@@ -84,6 +94,7 @@ export class VaultPopupItemsService {
this.cipherService.localData$, this.cipherService.localData$,
).pipe( ).pipe(
runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular 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(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
switchMap((ciphers) => switchMap((ciphers) =>
combineLatest([ combineLatest([
@@ -112,6 +123,7 @@ export class VaultPopupItemsService {
this._searchText$, this._searchText$,
this.vaultPopupListFiltersService.filterFunction$, this.vaultPopupListFiltersService.filterFunction$,
]).pipe( ]).pipe(
tap(() => this._ciphersLoading$.next()),
map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [ map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [
filterFunction(ciphers), filterFunction(ciphers),
searchText, searchText,
@@ -148,10 +160,8 @@ export class VaultPopupItemsService {
* List of favorite ciphers that are not currently suggested for autofill. * List of favorite ciphers that are not currently suggested for autofill.
* Ciphers are sorted by last used date, then by name. * Ciphers are sorted by last used date, then by name.
*/ */
favoriteCiphers$: Observable<PopupCipherView[]> = combineLatest([ favoriteCiphers$: Observable<PopupCipherView[]> = this.autoFillCiphers$.pipe(
this.autoFillCiphers$, withLatestFrom(this._filteredCipherList$),
this._filteredCipherList$,
]).pipe(
map(([autoFillCiphers, ciphers]) => map(([autoFillCiphers, ciphers]) =>
ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)), 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. * List of all remaining ciphers that are not currently suggested for autofill or marked as favorite.
* Ciphers are sorted by name. * Ciphers are sorted by name.
*/ */
remainingCiphers$: Observable<PopupCipherView[]> = combineLatest([ remainingCiphers$: Observable<PopupCipherView[]> = this.favoriteCiphers$.pipe(
this.autoFillCiphers$, withLatestFrom(this._filteredCipherList$, this.autoFillCiphers$),
this.favoriteCiphers$, map(([favoriteCiphers, ciphers, autoFillCiphers]) =>
this._filteredCipherList$,
]).pipe(
map(([autoFillCiphers, favoriteCiphers, ciphers]) =>
ciphers.filter( ciphers.filter(
(cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher), (cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher),
), ),
@@ -179,6 +186,14 @@ export class VaultPopupItemsService {
shareReplay({ refCount: false, bufferSize: 1 }), shareReplay({ refCount: false, bufferSize: 1 }),
); );
/**
* Observable that indicates whether the service is currently loading ciphers.
*/
loading$: Observable<boolean> = 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. * Observable that indicates whether a filter is currently applied to the ciphers.
*/ */

View File

@@ -3,6 +3,8 @@ import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, skipWhile } from "rxjs"; import { BehaviorSubject, skipWhile } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductType } from "@bitwarden/common/enums"; import { ProductType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -23,6 +25,7 @@ describe("VaultPopupListFiltersService", () => {
const folderViews$ = new BehaviorSubject([]); const folderViews$ = new BehaviorSubject([]);
const cipherViews$ = new BehaviorSubject({}); const cipherViews$ = new BehaviorSubject({});
const decryptedCollections$ = new BehaviorSubject<CollectionView[]>([]); const decryptedCollections$ = new BehaviorSubject<CollectionView[]>([]);
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(false);
const collectionService = { const collectionService = {
decryptedCollections$, decryptedCollections$,
@@ -45,9 +48,15 @@ describe("VaultPopupListFiltersService", () => {
t: (key: string) => key, t: (key: string) => key,
} as I18nService; } as I18nService;
const policyService = {
policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$),
};
beforeEach(() => { beforeEach(() => {
memberOrganizations$.next([]); memberOrganizations$.next([]);
decryptedCollections$.next([]); decryptedCollections$.next([]);
policyAppliesToActiveUser$.next(false);
policyService.policyAppliesToActiveUser$.mockClear();
collectionService.getAllNested = () => Promise.resolve([]); collectionService.getAllNested = () => Promise.resolve([]);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -72,6 +81,10 @@ describe("VaultPopupListFiltersService", () => {
provide: CollectionService, provide: CollectionService,
useValue: collectionService, useValue: collectionService,
}, },
{
provide: PolicyService,
useValue: policyService,
},
{ provide: FormBuilder, useClass: FormBuilder }, { 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", () => { describe("icons", () => {
it("sets family icon for family organizations", (done) => { it("sets family icon for family organizations", (done) => {
const orgs = [ const orgs = [

View File

@@ -13,6 +13,8 @@ import {
import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; 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 { 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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductType } from "@bitwarden/common/enums"; import { ProductType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -88,6 +90,7 @@ export class VaultPopupListFiltersService {
private i18nService: I18nService, private i18nService: I18nService,
private collectionService: CollectionService, private collectionService: CollectionService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private policyService: PolicyService,
) { ) {
this.filterForm.controls.organization.valueChanges this.filterForm.controls.organization.valueChanges
.pipe(takeUntilDestroyed()) .pipe(takeUntilDestroyed())
@@ -167,44 +170,63 @@ export class VaultPopupListFiltersService {
/** /**
* Organization array structured to be directly passed to `ChipSelectComponent` * Organization array structured to be directly passed to `ChipSelectComponent`
*/ */
organizations$: Observable<ChipSelectOption<Organization>[]> = organizations$: Observable<ChipSelectOption<Organization>[]> = combineLatest([
this.organizationService.memberOrganizations$.pipe( this.organizationService.memberOrganizations$,
map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))), this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership),
map((orgs) => { ]).pipe(
if (!orgs.length) { map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [
return []; 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 there is only one organization and personal ownership policy applies,
// When the user is a member of an organization, make the "My Vault" option available // return an empty array, resulting in the org filter being hidden
{ if (orgs.length === 1 && personalOwnershipApplies) {
value: { id: MY_VAULT_ID } as Organization, return [];
label: this.i18nService.t("myVault"), }
icon: "bwi-user",
},
...orgs.map((org) => {
let icon = "bwi-business";
if (!org.enabled) { const myVaultOrg: ChipSelectOption<Organization>[] = [];
// 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 { // Only add "My vault" if personal ownership policy does not apply
value: org, if (!personalOwnershipApplies) {
label: org.name, myVaultOrg.push({
icon, 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` * Folder array structured to be directly passed to `ChipSelectComponent`

View File

@@ -80,7 +80,7 @@
"papaparse": "5.4.1", "papaparse": "5.4.1",
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tldts": "6.1.22", "tldts": "6.1.25",
"zxcvbn": "4.4.2" "zxcvbn": "4.4.2"
} }
} }

View File

@@ -9,8 +9,10 @@ export class OrganizationCollectionRequest extends CollectionExport {
req.name = "Collection name"; req.name = "Collection name";
req.externalId = null; req.externalId = null;
req.groups = [SelectionReadOnly.template(), SelectionReadOnly.template()]; req.groups = [SelectionReadOnly.template(), SelectionReadOnly.template()];
req.users = [SelectionReadOnly.template(), SelectionReadOnly.template()];
return req; return req;
} }
groups: SelectionReadOnly[]; groups: SelectionReadOnly[];
users: SelectionReadOnly[];
} }

View File

@@ -170,10 +170,17 @@ export class EditCommand {
: req.groups.map( : req.groups.map(
(g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage), (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(); const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString; request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
request.externalId = req.externalId; request.externalId = req.externalId;
request.groups = groups; request.groups = groups;
request.users = users;
const response = await this.apiService.putCollection(req.organizationId, id, request); const response = await this.apiService.putCollection(req.organizationId, id, request);
const view = CollectionExport.toView(req); const view = CollectionExport.toView(req);
view.id = response.id; view.id = response.id;

View File

@@ -87,6 +87,7 @@ export class ServeCommand {
this.serviceContainer.apiService, this.serviceContainer.apiService,
this.serviceContainer.folderApiService, this.serviceContainer.folderApiService,
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.organizationService,
); );
this.editCommand = new EditCommand( this.editCommand = new EditCommand(
this.serviceContainer.cipherService, this.serviceContainer.cipherService,

View File

@@ -226,6 +226,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.apiService, this.serviceContainer.apiService,
this.serviceContainer.folderApiService, this.serviceContainer.folderApiService,
this.serviceContainer.billingAccountProfileStateService, this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.organizationService,
); );
const response = await command.run(object, encodedJson, cmd); const response = await command.run(object, encodedJson, cmd);
this.processResponse(response); this.processResponse(response);

View File

@@ -4,6 +4,7 @@ import * as path from "path";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { 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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
@@ -32,6 +33,7 @@ export class CreateCommand {
private apiService: ApiService, private apiService: ApiService,
private folderApiService: FolderApiServiceAbstraction, private folderApiService: FolderApiServiceAbstraction,
private accountProfileService: BillingAccountProfileStateService, private accountProfileService: BillingAccountProfileStateService,
private organizationService: OrganizationService,
) {} ) {}
async run( async run(
@@ -183,6 +185,8 @@ export class CreateCommand {
if (orgKey == null) { if (orgKey == null) {
throw new Error("No encryption key for this organization."); throw new Error("No encryption key for this organization.");
} }
const organization = await this.organizationService.get(req.organizationId);
const currentOrgUserId = organization.organizationUserId;
const groups = const groups =
req.groups == null req.groups == null
@@ -190,10 +194,17 @@ export class CreateCommand {
: req.groups.map( : req.groups.map(
(g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage), (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(); const request = new CollectionRequest();
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString; request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
request.externalId = req.externalId; request.externalId = req.externalId;
request.groups = groups; request.groups = groups;
request.users = users;
const response = await this.apiService.postCollection(req.organizationId, request); const response = await this.apiService.postCollection(req.organizationId, request);
const view = CollectionExport.toView(req); const view = CollectionExport.toView(req);
view.id = response.id; view.id = response.id;

View File

@@ -80,7 +80,6 @@ export class InternalGroupService extends GroupService {
async save(group: GroupView): Promise<GroupView> { async save(group: GroupView): Promise<GroupView> {
const request = new GroupRequest(); const request = new GroupRequest();
request.name = group.name; request.name = group.name;
request.accessAll = group.accessAll;
request.users = group.members; request.users = group.members;
request.collections = group.collections.map( request.collections = group.collections.map(
(c) => new SelectionReadOnlyRequest(c.id, c.readOnly, c.hidePasswords, c.manage), (c) => new SelectionReadOnlyRequest(c.id, c.readOnly, c.hidePasswords, c.manage),

View File

@@ -2,7 +2,6 @@ import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models
export class GroupRequest { export class GroupRequest {
name: string; name: string;
accessAll: boolean;
collections: SelectionReadOnlyRequest[] = []; collections: SelectionReadOnlyRequest[] = [];
users: string[] = []; users: string[] = [];
} }

View File

@@ -5,11 +5,6 @@ export class GroupResponse extends BaseResponse {
id: string; id: string;
organizationId: string; organizationId: string;
name: string; name: string;
/**
* @deprecated
* To be removed after Flexible Collections.
**/
accessAll: boolean;
externalId: string; externalId: string;
constructor(response: any) { constructor(response: any) {
@@ -17,7 +12,6 @@ export class GroupResponse extends BaseResponse {
this.id = this.getResponseProperty("Id"); this.id = this.getResponseProperty("Id");
this.organizationId = this.getResponseProperty("OrganizationId"); this.organizationId = this.getResponseProperty("OrganizationId");
this.name = this.getResponseProperty("Name"); this.name = this.getResponseProperty("Name");
this.accessAll = this.getResponseProperty("AccessAll");
this.externalId = this.getResponseProperty("ExternalId"); this.externalId = this.getResponseProperty("ExternalId");
} }
} }

View File

@@ -41,7 +41,6 @@ export class UserAdminService {
async save(user: OrganizationUserAdminView): Promise<void> { async save(user: OrganizationUserAdminView): Promise<void> {
const request = new OrganizationUserUpdateRequest(); const request = new OrganizationUserUpdateRequest();
request.accessAll = user.accessAll;
request.permissions = user.permissions; request.permissions = user.permissions;
request.type = user.type; request.type = user.type;
request.collections = user.collections; request.collections = user.collections;
@@ -54,7 +53,6 @@ export class UserAdminService {
async invite(emails: string[], user: OrganizationUserAdminView): Promise<void> { async invite(emails: string[], user: OrganizationUserAdminView): Promise<void> {
const request = new OrganizationUserInviteRequest(); const request = new OrganizationUserInviteRequest();
request.emails = emails; request.emails = emails;
request.accessAll = user.accessAll;
request.permissions = user.permissions; request.permissions = user.permissions;
request.type = user.type; request.type = user.type;
request.collections = user.collections; request.collections = user.collections;
@@ -77,7 +75,6 @@ export class UserAdminService {
view.type = u.type; view.type = u.type;
view.status = u.status; view.status = u.status;
view.externalId = u.externalId; view.externalId = u.externalId;
view.accessAll = u.accessAll;
view.permissions = u.permissions; view.permissions = u.permissions;
view.resetPasswordEnrolled = u.resetPasswordEnrolled; view.resetPasswordEnrolled = u.resetPasswordEnrolled;
view.collections = u.collections.map((c) => ({ view.collections = u.collections.map((c) => ({

View File

@@ -8,12 +8,6 @@ export class GroupView implements View {
id: string; id: string;
organizationId: string; organizationId: string;
name: string; name: string;
/**
* @deprecated
* To be removed after Flexible Collections.
* This will always return `false` if Flexible Collections is enabled.
**/
accessAll: boolean;
externalId: string; externalId: string;
collections: CollectionAccessSelectionView[] = []; collections: CollectionAccessSelectionView[] = [];
members: string[] = []; members: string[] = [];

View File

@@ -13,12 +13,6 @@ export class OrganizationUserAdminView {
type: OrganizationUserType; type: OrganizationUserType;
status: OrganizationUserStatusType; status: OrganizationUserStatusType;
externalId: string; externalId: string;
/**
* @deprecated
* To be removed after Flexible Collections.
* This will always return `false` if Flexible Collections is enabled.
**/
accessAll: boolean;
permissions: PermissionsApi; permissions: PermissionsApi;
resetPasswordEnrolled: boolean; resetPasswordEnrolled: boolean;
hasMasterPassword: boolean; hasMasterPassword: boolean;

View File

@@ -12,12 +12,6 @@ export class OrganizationUserView {
userId: string; userId: string;
type: OrganizationUserType; type: OrganizationUserType;
status: OrganizationUserStatusType; status: OrganizationUserStatusType;
/**
* @deprecated
* To be removed after Flexible Collections.
* This will always return `false` if Flexible Collections is enabled.
**/
accessAll: boolean;
permissions: PermissionsApi; permissions: PermissionsApi;
resetPasswordEnrolled: boolean; resetPasswordEnrolled: boolean;
name: string; name: string;

View File

@@ -11,7 +11,7 @@
<bit-nav-item <bit-nav-item
icon="bwi-collection" icon="bwi-collection"
[text]="(organization.flexibleCollections ? 'collections' : 'vault') | i18n" [text]="'collections' | i18n"
route="vault" route="vault"
*ngIf="canShowVaultTab(organization)" *ngIf="canShowVaultTab(organization)"
> >

View File

@@ -45,7 +45,6 @@
[columnHeader]="'member' | i18n" [columnHeader]="'member' | i18n"
[selectorLabelText]="'selectMembers' | i18n" [selectorLabelText]="'selectMembers' | i18n"
[emptySelectionText]="'noMembersAdded' | i18n" [emptySelectionText]="'noMembersAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector> ></bit-access-selector>
</bit-tab> </bit-tab>
@@ -56,24 +55,14 @@
{{ "restrictedCollectionAssignmentDesc" | i18n }} {{ "restrictedCollectionAssignmentDesc" | i18n }}
</span> </span>
</p> </p>
<div *ngIf="!(flexibleCollectionsEnabled$ | async)" class="tw-my-3"> <bit-access-selector
<input type="checkbox" formControlName="accessAll" id="accessAll" /> formControlName="collections"
<label class="tw-mb-0 tw-text-lg" for="accessAll">{{ [items]="collections"
"accessAllCollectionsDesc" | i18n [permissionMode]="PermissionMode.Edit"
}}</label> [columnHeader]="'collection' | i18n"
<p class="tw-my-0 tw-text-muted">{{ "accessAllCollectionsHelp" | i18n }}</p> [selectorLabelText]="'selectCollections' | i18n"
</div> [emptySelectionText]="'noCollectionsAdded' | i18n"
<ng-container *ngIf="!groupForm.value.accessAll"> ></bit-access-selector>
<bit-access-selector
formControlName="collections"
[items]="collections"
[permissionMode]="PermissionMode.Edit"
[columnHeader]="'collection' | i18n"
[selectorLabelText]="'selectCollections' | i18n"
[emptySelectionText]="'noCollectionsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector>
</ng-container>
</bit-tab> </bit-tab>
</bit-tab-group> </bit-tab-group>
</div> </div>

View File

@@ -96,9 +96,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
private organization$ = this.organizationService private organization$ = this.organizationService
.get$(this.organizationId) .get$(this.organizationId)
.pipe(shareReplay({ refCount: true })); .pipe(shareReplay({ refCount: true }));
protected flexibleCollectionsEnabled$ = this.organization$.pipe(
map((o) => o?.flexibleCollections),
);
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1, FeatureFlag.FlexibleCollectionsV1,
); );
@@ -114,7 +111,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
group: GroupView; group: GroupView;
groupForm = this.formBuilder.group({ groupForm = this.formBuilder.group({
accessAll: [false],
name: ["", [Validators.required, Validators.maxLength(100)]], name: ["", [Validators.required, Validators.maxLength(100)]],
externalId: this.formBuilder.control({ value: "", disabled: true }), externalId: this.formBuilder.control({ value: "", disabled: true }),
members: [[] as AccessItemValue[]], members: [[] as AccessItemValue[]],
@@ -188,7 +184,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
this.flexibleCollectionsV1Enabled$, this.flexibleCollectionsV1Enabled$,
]).pipe( ]).pipe(
map(([organization, flexibleCollectionsV1Enabled]) => { map(([organization, flexibleCollectionsV1Enabled]) => {
if (!flexibleCollectionsV1Enabled || !organization.flexibleCollections) { if (!flexibleCollectionsV1Enabled) {
return true; return true;
} }
@@ -276,7 +272,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
this.groupForm.patchValue({ this.groupForm.patchValue({
name: this.group.name, name: this.group.name,
externalId: this.group.externalId, externalId: this.group.externalId,
accessAll: this.group.accessAll,
members: this.group.members.map((m) => ({ members: this.group.members.map((m) => ({
id: m, id: m,
type: AccessItemType.Member, type: AccessItemType.Member,
@@ -328,12 +323,8 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
const formValue = this.groupForm.value; const formValue = this.groupForm.value;
groupView.name = formValue.name; groupView.name = formValue.name;
groupView.accessAll = formValue.accessAll;
groupView.members = formValue.members?.map((m) => m.id) ?? []; groupView.members = formValue.members?.map((m) => m.id) ?? [];
groupView.collections = formValue.collections.map((c) => convertToSelectionView(c));
if (!groupView.accessAll) {
groupView.collections = formValue.collections.map((c) => convertToSelectionView(c));
}
await this.groupService.save(groupView); await this.groupService.save(groupView);

View File

@@ -74,12 +74,10 @@
</td> </td>
<td bitCell (click)="edit(g, ModalTabType.Collections)" class="tw-cursor-pointer"> <td bitCell (click)="edit(g, ModalTabType.Collections)" class="tw-cursor-pointer">
<bit-badge-list <bit-badge-list
*ngIf="!g.details.accessAll"
[items]="g.collectionNames" [items]="g.collectionNames"
[maxItems]="3" [maxItems]="3"
variant="secondary" variant="secondary"
></bit-badge-list> ></bit-badge-list>
<span *ngIf="g.details.accessAll">{{ "all" | i18n }}</span>
</td> </td>
<td bitCell> <td bitCell>
<button <button

View File

@@ -49,14 +49,6 @@
<bit-label>{{ "user" | i18n }}</bit-label> <bit-label>{{ "user" | i18n }}</bit-label>
<bit-hint>{{ "userDesc" | i18n }}</bit-hint> <bit-hint>{{ "userDesc" | i18n }}</bit-hint>
</bit-radio-button> </bit-radio-button>
<bit-radio-button
*ngIf="!organization.flexibleCollections"
id="userTypeManager"
[value]="organizationUserType.Manager"
>
<bit-label>{{ "manager" | i18n }}</bit-label>
<bit-hint>{{ "managerDesc" | i18n }}</bit-hint>
</bit-radio-button>
<bit-radio-button id="userTypeAdmin" [value]="organizationUserType.Admin"> <bit-radio-button id="userTypeAdmin" [value]="organizationUserType.Admin">
<bit-label>{{ "admin" | i18n }}</bit-label> <bit-label>{{ "admin" | i18n }}</bit-label>
<bit-hint>{{ "adminDesc" | i18n }}</bit-hint> <bit-hint>{{ "adminDesc" | i18n }}</bit-hint>
@@ -91,140 +83,64 @@
</bit-radio-button> </bit-radio-button>
</bit-radio-group> </bit-radio-group>
<ng-container *ngIf="customUserTypeSelected"> <ng-container *ngIf="customUserTypeSelected">
<ng-container *ngIf="!organization.flexibleCollections; else customPermissionsFC"> <div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
<h3 bitTypography="h3"> <div class="tw-col-span-4">
{{ "permissions" | i18n }} <bit-form-control>
</h3> <input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup"> <bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
<div class="tw-col-span-6"> </bit-form-control>
<div class="tw-mb-3"> <bit-form-control>
<bit-label class="tw-font-semibold">{{ <input type="checkbox" bitCheckbox formControlName="accessImportExport" />
"managerPermissions" | i18n <bit-label>{{ "accessImportExport" | i18n }}</bit-label>
}}</bit-label> </bit-form-control>
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" /> <bit-form-control>
<app-nested-checkbox <input type="checkbox" bitCheckbox formControlName="accessReports" />
parentId="manageAssignedCollections" <bit-label>{{ "accessReports" | i18n }}</bit-label>
[checkboxes]="permissionsGroup.controls.manageAssignedCollectionsGroup" </bit-form-control>
> </div>
</app-nested-checkbox> <div class="tw-col-span-4">
</div> <app-nested-checkbox
</div> parentId="manageAllCollections"
<div class="tw-col-span-6"> [checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
<div class="tw-mb-3"> >
<bit-label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</bit-label> </app-nested-checkbox>
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" /> </div>
<bit-form-control> <div class="tw-col-span-4">
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" /> <div class="tw-mb-3">
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label> <bit-form-control>
</bit-form-control> <input type="checkbox" bitCheckbox formControlName="manageGroups" />
<bit-form-control> <bit-label>{{ "manageGroups" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="accessImportExport" /> </bit-form-control>
<bit-label>{{ "accessImportExport" | i18n }}</bit-label> <bit-form-control>
</bit-form-control> <input type="checkbox" bitCheckbox formControlName="manageSso" />
<bit-form-control> <bit-label>{{ "manageSso" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="accessReports" /> </bit-form-control>
<bit-label>{{ "accessReports" | i18n }}</bit-label> <bit-form-control>
</bit-form-control> <input type="checkbox" bitCheckbox formControlName="managePolicies" />
<app-nested-checkbox <bit-label>{{ "managePolicies" | i18n }}</bit-label>
parentId="manageAllCollections" </bit-form-control>
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup" <bit-form-control>
> <input
</app-nested-checkbox> id="manageUsers"
<bit-form-control> type="checkbox"
<input type="checkbox" bitCheckbox formControlName="manageGroups" /> bitCheckbox
<bit-label>{{ "manageGroups" | i18n }}</bit-label> formControlName="manageUsers"
</bit-form-control> (change)="handleDependentPermissions()"
<bit-form-control> />
<input type="checkbox" bitCheckbox formControlName="manageSso" /> <bit-label>{{ "manageUsers" | i18n }}</bit-label>
<bit-label>{{ "manageSso" | i18n }}</bit-label> </bit-form-control>
</bit-form-control> <bit-form-control>
<bit-form-control> <input
<input type="checkbox" bitCheckbox formControlName="managePolicies" /> type="checkbox"
<bit-label>{{ "managePolicies" | i18n }}</bit-label> bitCheckbox
</bit-form-control> formControlName="manageResetPassword"
<bit-form-control> (change)="handleDependentPermissions()"
<input />
id="manageUsers" <bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
type="checkbox" </bit-form-control>
bitCheckbox
formControlName="manageUsers"
(change)="handleDependentPermissions()"
/>
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input
type="checkbox"
bitCheckbox
formControlName="manageResetPassword"
(change)="handleDependentPermissions()"
/>
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
</bit-form-control>
</div>
</div> </div>
</div> </div>
</ng-container> </div>
<ng-template #customPermissionsFC>
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
<div class="tw-col-span-4">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="accessEventLogs" />
<bit-label>{{ "accessEventLogs" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="accessImportExport" />
<bit-label>{{ "accessImportExport" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="accessReports" />
<bit-label>{{ "accessReports" | i18n }}</bit-label>
</bit-form-control>
</div>
<div class="tw-col-span-4">
<app-nested-checkbox
parentId="manageAllCollections"
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
>
</app-nested-checkbox>
</div>
<div class="tw-col-span-4">
<div class="tw-mb-3">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="manageGroups" />
<bit-label>{{ "manageGroups" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="manageSso" />
<bit-label>{{ "manageSso" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="managePolicies" />
<bit-label>{{ "managePolicies" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input
id="manageUsers"
type="checkbox"
bitCheckbox
formControlName="manageUsers"
(change)="handleDependentPermissions()"
/>
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input
type="checkbox"
bitCheckbox
formControlName="manageResetPassword"
(change)="handleDependentPermissions()"
/>
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
</bit-form-control>
</div>
</div>
</div>
</ng-template>
</ng-container> </ng-container>
<ng-container *ngIf="organization.useSecretsManager"> <ng-container *ngIf="organization.useSecretsManager">
<h3 class="tw-mt-4"> <h3 class="tw-mt-4">
@@ -272,7 +188,6 @@
[columnHeader]="'groups' | i18n" [columnHeader]="'groups' | i18n"
[selectorLabelText]="'selectGroups' | i18n" [selectorLabelText]="'selectGroups' | i18n"
[emptySelectionText]="'noGroupsAdded' | i18n" [emptySelectionText]="'noGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections"
[hideMultiSelect]="restrictEditingSelf$ | async" [hideMultiSelect]="restrictEditingSelf$ | async"
></bit-access-selector> ></bit-access-selector>
</bit-tab> </bit-tab>
@@ -294,26 +209,7 @@
{{ "restrictedCollectionAssignmentDesc" | i18n }} {{ "restrictedCollectionAssignmentDesc" | i18n }}
</span> </span>
</div> </div>
<div *ngIf="!organization.flexibleCollections" class="tw-mb-6">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="accessAllCollections" />
<bit-label>
{{ "accessAllCollectionsDesc" | i18n }}
<a
bitLink
target="_blank"
rel="noreferrer"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/user-types-access-control/#access-control"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</bit-label>
<bit-hint>{{ "accessAllCollectionsHelp" | i18n }}</bit-hint>
</bit-form-control>
</div>
<bit-access-selector <bit-access-selector
*ngIf="!accessAllCollections"
[permissionMode]="PermissionMode.Edit" [permissionMode]="PermissionMode.Edit"
formControlName="access" formControlName="access"
[showGroupColumn]="organization.useGroups" [showGroupColumn]="organization.useGroups"
@@ -321,7 +217,6 @@
[columnHeader]="'collection' | i18n" [columnHeader]="'collection' | i18n"
[selectorLabelText]="'selectCollections' | i18n" [selectorLabelText]="'selectCollections' | i18n"
[emptySelectionText]="'noCollectionsAdded' | i18n" [emptySelectionText]="'noCollectionsAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections"
[hideMultiSelect]="restrictEditingSelf$ | async" [hideMultiSelect]="restrictEditingSelf$ | async"
></bit-access-selector ></bit-access-selector
></bit-tab> ></bit-tab>

View File

@@ -99,7 +99,6 @@ export class MemberDialogComponent implements OnDestroy {
emails: [""], emails: [""],
type: OrganizationUserType.User, type: OrganizationUserType.User,
externalId: this.formBuilder.control({ value: "", disabled: true }), externalId: this.formBuilder.control({ value: "", disabled: true }),
accessAllCollections: false,
accessSecretsManager: false, accessSecretsManager: false,
access: [[] as AccessItemValue[]], access: [[] as AccessItemValue[]],
groups: [[] as AccessItemValue[]], groups: [[] as AccessItemValue[]],
@@ -110,11 +109,6 @@ export class MemberDialogComponent implements OnDestroy {
protected canAssignAccessToAnyCollection$: Observable<boolean>; protected canAssignAccessToAnyCollection$: Observable<boolean>;
protected permissionsGroup = this.formBuilder.group({ protected permissionsGroup = this.formBuilder.group({
manageAssignedCollectionsGroup: this.formBuilder.group<Record<string, boolean>>({
manageAssignedCollections: false,
editAssignedCollections: false,
deleteAssignedCollections: false,
}),
manageAllCollectionsGroup: this.formBuilder.group<Record<string, boolean>>({ manageAllCollectionsGroup: this.formBuilder.group<Record<string, boolean>>({
manageAllCollections: false, manageAllCollections: false,
createNewCollections: false, createNewCollections: false,
@@ -137,10 +131,6 @@ export class MemberDialogComponent implements OnDestroy {
return this.formGroup.value.type === OrganizationUserType.Custom; return this.formGroup.value.type === OrganizationUserType.Custom;
} }
get accessAllCollections(): boolean {
return this.formGroup.value.accessAllCollections;
}
constructor( constructor(
@Inject(DIALOG_DATA) protected params: MemberDialogParams, @Inject(DIALOG_DATA) protected params: MemberDialogParams,
private dialogRef: DialogRef<MemberDialogResult>, private dialogRef: DialogRef<MemberDialogResult>,
@@ -189,7 +179,7 @@ export class MemberDialogComponent implements OnDestroy {
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
]).pipe( ]).pipe(
map(([organization, flexibleCollectionsV1Enabled]) => { map(([organization, flexibleCollectionsV1Enabled]) => {
if (!flexibleCollectionsV1Enabled || !organization.flexibleCollections) { if (!flexibleCollectionsV1Enabled) {
return true; return true;
} }
@@ -316,13 +306,6 @@ export class MemberDialogComponent implements OnDestroy {
this.showNoMasterPasswordWarning = this.showNoMasterPasswordWarning =
userDetails.status > OrganizationUserStatusType.Invited && userDetails.status > OrganizationUserStatusType.Invited &&
userDetails.hasMasterPassword === false; userDetails.hasMasterPassword === false;
const assignedCollectionsPermissions = {
editAssignedCollections: userDetails.permissions.editAssignedCollections,
deleteAssignedCollections: userDetails.permissions.deleteAssignedCollections,
manageAssignedCollections:
userDetails.permissions.editAssignedCollections &&
userDetails.permissions.deleteAssignedCollections,
};
const allCollectionsPermissions = { const allCollectionsPermissions = {
createNewCollections: userDetails.permissions.createNewCollections, createNewCollections: userDetails.permissions.createNewCollections,
editAnyCollection: userDetails.permissions.editAnyCollection, editAnyCollection: userDetails.permissions.editAnyCollection,
@@ -342,7 +325,6 @@ export class MemberDialogComponent implements OnDestroy {
managePolicies: userDetails.permissions.managePolicies, managePolicies: userDetails.permissions.managePolicies,
manageUsers: userDetails.permissions.manageUsers, manageUsers: userDetails.permissions.manageUsers,
manageResetPassword: userDetails.permissions.manageResetPassword, manageResetPassword: userDetails.permissions.manageResetPassword,
manageAssignedCollectionsGroup: assignedCollectionsPermissions,
manageAllCollectionsGroup: allCollectionsPermissions, manageAllCollectionsGroup: allCollectionsPermissions,
}); });
} }
@@ -378,7 +360,6 @@ export class MemberDialogComponent implements OnDestroy {
this.formGroup.patchValue({ this.formGroup.patchValue({
type: userDetails.type, type: userDetails.type,
externalId: userDetails.externalId, externalId: userDetails.externalId,
accessAllCollections: userDetails.accessAll,
access: accessSelections, access: accessSelections,
accessSecretsManager: userDetails.accessSecretsManager, accessSecretsManager: userDetails.accessSecretsManager,
groups: groupAccessSelections, groups: groupAccessSelections,
@@ -414,10 +395,6 @@ export class MemberDialogComponent implements OnDestroy {
editAnyCollection: this.permissionsGroup.value.manageAllCollectionsGroup.editAnyCollection, editAnyCollection: this.permissionsGroup.value.manageAllCollectionsGroup.editAnyCollection,
deleteAnyCollection: deleteAnyCollection:
this.permissionsGroup.value.manageAllCollectionsGroup.deleteAnyCollection, this.permissionsGroup.value.manageAllCollectionsGroup.deleteAnyCollection,
editAssignedCollections:
this.permissionsGroup.value.manageAssignedCollectionsGroup.editAssignedCollections,
deleteAssignedCollections:
this.permissionsGroup.value.manageAssignedCollectionsGroup.deleteAssignedCollections,
}; };
return Object.assign(p, partialPermissions); return Object.assign(p, partialPermissions);
@@ -467,7 +444,6 @@ export class MemberDialogComponent implements OnDestroy {
const userView = new OrganizationUserAdminView(); const userView = new OrganizationUserAdminView();
userView.id = this.params.organizationUserId; userView.id = this.params.organizationUserId;
userView.organizationId = this.params.organizationId; userView.organizationId = this.params.organizationId;
userView.accessAll = this.accessAllCollections;
userView.type = this.formGroup.value.type; userView.type = this.formGroup.value.type;
userView.permissions = this.setRequestPermissions( userView.permissions = this.setRequestPermissions(
userView.permissions ?? new PermissionsApi(), userView.permissions ?? new PermissionsApi(),

View File

@@ -190,12 +190,10 @@
class="tw-cursor-pointer" class="tw-cursor-pointer"
> >
<bit-badge-list <bit-badge-list
*ngIf="organization.useGroups || !u.accessAll"
[items]="organization.useGroups ? u.groupNames : u.collectionNames" [items]="organization.useGroups ? u.groupNames : u.collectionNames"
[maxItems]="3" [maxItems]="3"
variant="secondary" variant="secondary"
></bit-badge-list> ></bit-badge-list>
<span *ngIf="!organization.useGroups && u.accessAll">{{ "all" | i18n }}</span>
</td> </td>
<td <td

View File

@@ -51,7 +51,7 @@
</button> </button>
</ng-container> </ng-container>
<form <form
*ngIf="org && !loading && org.flexibleCollections" *ngIf="org && !loading"
[bitSubmit]="submitCollectionManagement" [bitSubmit]="submitCollectionManagement"
[formGroup]="collectionManagementFormGroup" [formGroup]="collectionManagementFormGroup"
> >

View File

@@ -110,15 +110,6 @@
</ng-container> </ng-container>
<ng-template #readOnlyPerm> <ng-template #readOnlyPerm>
<div
*ngIf="item.accessAllItems"
class="tw-max-w-40 tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap tw-border tw-border-solid tw-border-transparent tw-font-bold tw-text-muted"
[appA11yTitle]="accessAllLabelId(item) | i18n"
>
{{ "canEdit" | i18n }}
<i class="bwi bwi-filter tw-ml-1" aria-hidden="true"></i>
</div>
<div <div
*ngIf="item.readonly || disabled" *ngIf="item.readonly || disabled"
class="tw-max-w-40 tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap tw-font-bold tw-text-muted" class="tw-max-w-40 tw-overflow-hidden tw-overflow-ellipsis tw-whitespace-nowrap tw-font-bold tw-text-muted"

View File

@@ -75,7 +75,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
// The enable() above also enables the permission control, so we need to disable it again // The enable() above also enables the permission control, so we need to disable it again
// Disable permission control if accessAllItems is enabled or not in Edit mode // Disable permission control if accessAllItems is enabled or not in Edit mode
if (item.accessAllItems || this.permissionMode != PermissionMode.Edit) { if (this.permissionMode != PermissionMode.Edit) {
controlRow.controls.permission.disable(); controlRow.controls.permission.disable();
} }
} }
@@ -196,21 +196,11 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
*/ */
@Input() showGroupColumn: boolean; @Input() showGroupColumn: boolean;
/**
* Enable Flexible Collections changes (feature flag)
*/
@Input() set flexibleCollectionsEnabled(value: boolean) {
this._flexibleCollectionsEnabled = value;
this.permissionList = getPermissionList(value);
}
/** /**
* Hide the multi-select so that new items cannot be added * Hide the multi-select so that new items cannot be added
*/ */
@Input() hideMultiSelect = false; @Input() hideMultiSelect = false;
private _flexibleCollectionsEnabled: boolean;
constructor( constructor(
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly i18nService: I18nService, private readonly i18nService: I18nService,
@@ -275,7 +265,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
} }
async ngOnInit() { async ngOnInit() {
this.permissionList = getPermissionList(this._flexibleCollectionsEnabled); this.permissionList = getPermissionList();
// Watch the internal formArray for changes and propagate them // Watch the internal formArray for changes and propagate them
this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => { this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => {
if (!this.notifyOnChange || this.pauseChangeNotification) { if (!this.notifyOnChange || this.pauseChangeNotification) {
@@ -328,12 +318,8 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
return this.permissionList.find((p) => p.perm == perm)?.labelId; return this.permissionList.find((p) => p.perm == perm)?.labelId;
} }
protected accessAllLabelId(item: AccessItemView) {
return item.type == AccessItemType.Group ? "groupAccessAll" : "memberAccessAll";
}
protected canEditItemPermission(item: AccessItemView) { protected canEditItemPermission(item: AccessItemView) {
return this.permissionMode == PermissionMode.Edit && !item.readonly && !item.accessAllItems; return this.permissionMode == PermissionMode.Edit && !item.readonly;
} }
private _itemComparator(a: AccessItemView, b: AccessItemView) { private _itemComparator(a: AccessItemView, b: AccessItemView) {

View File

@@ -34,12 +34,6 @@ export enum AccessItemType {
* *
*/ */
export type AccessItemView = SelectItemView & { export type AccessItemView = SelectItemView & {
/**
* Flag that this group/member can access all items.
* This will disable the permission editor for this item.
*/
accessAllItems?: boolean;
/** /**
* Flag that this item cannot be modified. * Flag that this item cannot be modified.
* This will disable the permission editor and will keep * This will disable the permission editor and will keep
@@ -82,16 +76,14 @@ export type Permission = {
labelId: string; labelId: string;
}; };
export const getPermissionList = (flexibleCollectionsEnabled: boolean): Permission[] => { export const getPermissionList = (): Permission[] => {
const permissions = [ const permissions = [
{ perm: CollectionPermission.View, labelId: "canView" }, { perm: CollectionPermission.View, labelId: "canView" },
{ perm: CollectionPermission.ViewExceptPass, labelId: "canViewExceptPass" }, { perm: CollectionPermission.ViewExceptPass, labelId: "canViewExceptPass" },
{ perm: CollectionPermission.Edit, labelId: "canEdit" }, { perm: CollectionPermission.Edit, labelId: "canEdit" },
{ perm: CollectionPermission.EditExceptPass, labelId: "canEditExceptPass" }, { perm: CollectionPermission.EditExceptPass, labelId: "canEditExceptPass" },
{ perm: CollectionPermission.Manage, labelId: "canManage" },
]; ];
if (flexibleCollectionsEnabled) {
permissions.push({ perm: CollectionPermission.Manage, labelId: "canManage" });
}
return permissions; return permissions;
}; };
@@ -142,8 +134,6 @@ export function mapGroupToAccessItemView(group: GroupView): AccessItemView {
type: AccessItemType.Group, type: AccessItemType.Group,
listName: group.name, listName: group.name,
labelName: group.name, labelName: group.name,
accessAllItems: group.accessAll,
readonly: group.accessAll,
}; };
} }
@@ -157,7 +147,5 @@ export function mapUserToAccessItemView(user: OrganizationUserUserDetailsRespons
listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email, listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email,
labelName: user.name ?? user.email, labelName: user.name ?? user.email,
status: user.status, status: user.status,
accessAllItems: user.accessAll,
readonly: user.accessAll,
}; };
} }

View File

@@ -253,7 +253,6 @@ MemberGroupAccess.args = {
type: AccessItemType.Group, type: AccessItemType.Group,
listName: "Admin Group", listName: "Admin Group",
labelName: "Admin Group", labelName: "Admin Group",
accessAllItems: true,
}, },
]), ]),
}; };
@@ -309,7 +308,6 @@ CollectionAccess.args = {
type: AccessItemType.Group, type: AccessItemType.Group,
listName: "Admin Group", listName: "Admin Group",
labelName: "Admin Group", labelName: "Admin Group",
accessAllItems: true,
readonly: true, readonly: true,
}, },
{ {
@@ -320,7 +318,6 @@ CollectionAccess.args = {
status: OrganizationUserStatusType.Confirmed, status: OrganizationUserStatusType.Confirmed,
role: OrganizationUserType.Admin, role: OrganizationUserType.Admin,
email: "admin@email.com", email: "admin@email.com",
accessAllItems: true,
readonly: true, readonly: true,
}, },
]), ]),

View File

@@ -20,8 +20,6 @@ export class UserTypePipe implements PipeTransform {
return this.i18nService.t("admin"); return this.i18nService.t("admin");
case OrganizationUserType.User: case OrganizationUserType.User:
return this.i18nService.t("user"); return this.i18nService.t("user");
case OrganizationUserType.Manager:
return this.i18nService.t("manager");
case OrganizationUserType.Custom: case OrganizationUserType.Custom:
return this.i18nService.t("custom"); return this.i18nService.t("custom");
} }

View File

@@ -1,55 +1,33 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading"> <div *ngIf="loading" class="tw-text-center">
<div> <i
<img class="mb-4 logo logo-themed" alt="Bitwarden" /> class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
<p class="text-center"> title="{{ 'loading' | i18n }}"
<i aria-hidden="true"
class="bwi bwi-spinner bwi-spin bwi-2x text-muted" ></i>
title="{{ 'loading' | i18n }}" <span class="tw-sr-only">{{ "loading" | i18n }}</span>
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</div>
</div> </div>
<div class="container" *ngIf="!loading">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "removeMasterPassword" | i18n }}</p>
<hr />
<div class="card d-block">
<div class="card-body">
<p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p>
<button <div *ngIf="!loading">
type="button" <p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p>
class="btn btn-primary btn-block"
(click)="convert()" <button
[disabled]="actionPromise" bitButton
> type="button"
<i buttonType="primary"
class="bwi bwi-spinner bwi-spin" class="tw-w-full"
title="{{ 'loading' | i18n }}" [bitAction]="convert"
aria-hidden="true" [block]="true"
*ngIf="continuing" >
></i> {{ "removeMasterPassword" | i18n }}
{{ "removeMasterPassword" | i18n }} </button>
</button> <button
<button bitButton
type="button" type="button"
class="btn btn-outline-secondary btn-block" buttonType="secondary"
(click)="leave()" class="tw-w-full"
[disabled]="actionPromise" [bitAction]="leave"
> [block]="true"
<i >
class="bwi bwi-spinner bwi-spin" {{ "leaveOrganization" | i18n }}
title="{{ 'loading' | i18n }}" </button>
aria-hidden="true"
*ngIf="leaving"
></i>
{{ "leaveOrganization" | i18n }}
</button>
</div>
</div>
</div>
</div>
</div> </div>

View File

@@ -176,12 +176,6 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
data: { titleId: "updatePassword" } satisfies DataProperties, data: { titleId: "updatePassword" } satisfies DataProperties,
}, },
{
path: "remove-password",
component: RemovePasswordComponent,
canActivate: [AuthGuard],
data: { titleId: "removeMasterPassword" } satisfies DataProperties,
},
{ {
path: "migrate-legacy-encryption", path: "migrate-legacy-encryption",
loadComponent: () => loadComponent: () =>
@@ -195,25 +189,6 @@ const routes: Routes = [
path: "", path: "",
component: AnonLayoutWrapperComponent, component: AnonLayoutWrapperComponent,
children: [ 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", path: "accept-emergency",
canActivate: [deepLinkGuard()], 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,
},
], ],
}, },
{ {

View File

@@ -64,7 +64,7 @@
</bit-form-field> </bit-form-field>
</bit-tab> </bit-tab>
<bit-tab label="{{ 'access' | i18n }}"> <bit-tab label="{{ 'access' | i18n }}">
<div class="tw-mb-3" *ngIf="organization.flexibleCollections"> <div class="tw-mb-3">
<ng-container *ngIf="dialogReadonly"> <ng-container *ngIf="dialogReadonly">
<span>{{ "readOnlyCollectionAccess" | i18n }}</span> <span>{{ "readOnlyCollectionAccess" | i18n }}</span>
</ng-container> </ng-container>
@@ -107,7 +107,6 @@
[selectorLabelText]="'selectGroupsAndMembers' | i18n" [selectorLabelText]="'selectGroupsAndMembers' | i18n"
[selectorHelpText]="'userPermissionOverrideHelperDesc' | i18n" [selectorHelpText]="'userPermissionOverrideHelperDesc' | i18n"
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n" [emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections"
></bit-access-selector> ></bit-access-selector>
<bit-access-selector <bit-access-selector
*ngIf="!organization.useGroups" *ngIf="!organization.useGroups"
@@ -117,7 +116,6 @@
[columnHeader]="'memberColumnHeader' | i18n" [columnHeader]="'memberColumnHeader' | i18n"
[selectorLabelText]="'selectMembers' | i18n" [selectorLabelText]="'selectMembers' | i18n"
[emptySelectionText]="'noMembersAdded' | i18n" [emptySelectionText]="'noMembersAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections"
></bit-access-selector> ></bit-access-selector>
</bit-tab> </bit-tab>
</bit-tab-group> </bit-tab-group>

View File

@@ -223,7 +223,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
(u) => u.userId === this.organization?.userId, (u) => u.userId === this.organization?.userId,
)?.id; )?.id;
const initialSelection: AccessItemValue[] = const initialSelection: AccessItemValue[] =
currentOrgUserId !== undefined && organization.flexibleCollections currentOrgUserId !== undefined
? [ ? [
{ {
id: currentOrgUserId, id: currentOrgUserId,
@@ -239,11 +239,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
}); });
} }
if ( if (flexibleCollectionsV1 && !organization.allowAdminAccessToAllCollectionItems) {
organization.flexibleCollections &&
flexibleCollectionsV1 &&
!organization.allowAdminAccessToAllCollectionItems
) {
this.formGroup.controls.access.addValidators(validateCanManagePermission); this.formGroup.controls.access.addValidators(validateCanManagePermission);
} else { } else {
this.formGroup.controls.access.removeValidators(validateCanManagePermission); this.formGroup.controls.access.removeValidators(validateCanManagePermission);
@@ -444,8 +440,7 @@ function mapGroupToAccessItemView(group: GroupView, collectionId: string): Acces
type: AccessItemType.Group, type: AccessItemType.Group,
listName: group.name, listName: group.name,
labelName: group.name, labelName: group.name,
accessAllItems: group.accessAll, readonly: false,
readonly: group.accessAll,
readonlyPermission: readonlyPermission:
collectionId != null collectionId != null
? convertToPermission(group.collections.find((gc) => gc.id == collectionId)) ? 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, listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email,
labelName: user.name ?? user.email, labelName: user.name ?? user.email,
status: user.status, status: user.status,
accessAllItems: user.accessAll, readonly: false,
readonly: user.accessAll,
readonlyPermission: readonlyPermission:
collectionId != null collectionId != null
? convertToPermission( ? convertToPermission(

View File

@@ -86,7 +86,7 @@ export class VaultCollectionRowComponent {
return this.i18nService.t("canEdit"); return this.i18nService.t("canEdit");
} }
if ((this.collection as CollectionAdminView).assigned) { if ((this.collection as CollectionAdminView).assigned) {
const permissionList = getPermissionList(this.organization?.flexibleCollections); const permissionList = getPermissionList();
return this.i18nService.t( return this.i18nService.t(
permissionList.find((p) => p.perm === convertToPermission(this.collection))?.labelId, permissionList.find((p) => p.perm === convertToPermission(this.collection))?.labelId,
); );

View File

@@ -15,7 +15,6 @@ import { VaultFilter } from "../models/vault-filter.model";
}) })
export class VaultFilterSectionComponent implements OnInit, OnDestroy { export class VaultFilterSectionComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
protected flexibleCollectionsEnabled: boolean;
@Input() activeFilter: VaultFilter; @Input() activeFilter: VaultFilter;
@Input() section: VaultFilterSection; @Input() section: VaultFilterSection;
@@ -40,12 +39,6 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
this.section?.data$?.pipe(takeUntil(this.destroy$)).subscribe((data) => { this.section?.data$?.pipe(takeUntil(this.destroy$)).subscribe((data) => {
this.data = data; this.data = data;
}); });
this.vaultFilterService
.getOrganizationFilter()
.pipe(takeUntil(this.destroy$))
.subscribe((org) => {
this.flexibleCollectionsEnabled = org != null ? org.flexibleCollections : false;
});
} }
ngOnDestroy() { ngOnDestroy() {
@@ -77,10 +70,9 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
const { organizationId, cipherTypeId, folderId, collectionId, isCollectionSelected } = const { organizationId, cipherTypeId, folderId, collectionId, isCollectionSelected } =
this.activeFilter; this.activeFilter;
const collectionStatus = this.flexibleCollectionsEnabled const collectionStatus =
? filterNode?.node.id === "AllCollections" && filterNode?.node.id === "AllCollections" &&
(isCollectionSelected || collectionId === "AllCollections") (isCollectionSelected || collectionId === "AllCollections");
: collectionId === filterNode?.node.id;
return ( return (
organizationId === filterNode?.node.id || organizationId === filterNode?.node.id ||

View File

@@ -17,7 +17,6 @@
[selectorLabelText]="'selectGroupsAndMembers' | i18n" [selectorLabelText]="'selectGroupsAndMembers' | i18n"
[selectorHelpText]="'userPermissionOverrideHelperDesc' | i18n" [selectorHelpText]="'userPermissionOverrideHelperDesc' | i18n"
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n" [emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector> ></bit-access-selector>
<bit-access-selector <bit-access-selector
*ngIf="!organization?.useGroups" *ngIf="!organization?.useGroups"
@@ -27,7 +26,6 @@
[columnHeader]="'memberColumnHeader' | i18n" [columnHeader]="'memberColumnHeader' | i18n"
[selectorLabelText]="'selectMembers' | i18n" [selectorLabelText]="'selectMembers' | i18n"
[emptySelectionText]="'noMembersAdded' | i18n" [emptySelectionText]="'noMembersAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector> ></bit-access-selector>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject, OnDestroy } from "@angular/core"; import { Component, Inject, OnDestroy } from "@angular/core";
import { FormBuilder } from "@angular/forms"; 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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@@ -42,10 +42,6 @@ export enum BulkCollectionsDialogResult {
standalone: true, standalone: true,
}) })
export class BulkCollectionsDialogComponent implements OnDestroy { export class BulkCollectionsDialogComponent implements OnDestroy {
protected flexibleCollectionsEnabled$ = this.organizationService
.get$(this.params.organizationId)
.pipe(map((o) => o?.flexibleCollections));
protected readonly PermissionMode = PermissionMode; protected readonly PermissionMode = PermissionMode;
protected formGroup = this.formBuilder.group({ protected formGroup = this.formBuilder.group({

View File

@@ -103,11 +103,7 @@ export class VaultFilterComponent extends BaseVaultFilterComponent implements On
async buildAllFilters(): Promise<VaultFilterList> { async buildAllFilters(): Promise<VaultFilterList> {
const builderFilter = {} as VaultFilterList; const builderFilter = {} as VaultFilterList;
builderFilter.typeFilter = await this.addTypeFilter(["favorites"]); builderFilter.typeFilter = await this.addTypeFilter(["favorites"]);
if (this._organization?.flexibleCollections) { builderFilter.collectionFilter = await this.addCollectionFilter();
builderFilter.collectionFilter = await this.addCollectionFilter();
} else {
builderFilter.collectionFilter = await super.addCollectionFilter();
}
builderFilter.trashFilter = await this.addTrashFilter(); builderFilter.trashFilter = await this.addTrashFilter();
return builderFilter; return builderFilter;
} }

View File

@@ -6,10 +6,7 @@
queryParamsHandling="merge" queryParamsHandling="merge"
> >
{{ organization.name }} {{ organization.name }}
<span *ngIf="!organization.flexibleCollections"> <span>
{{ "vault" | i18n | lowercase }}
</span>
<span *ngIf="organization.flexibleCollections">
{{ "collections" | i18n | lowercase }} {{ "collections" | i18n | lowercase }}
</span> </span>
</bit-breadcrumb> </bit-breadcrumb>

View File

@@ -89,9 +89,7 @@ export class VaultHeaderComponent implements OnInit {
} }
get title() { get title() {
const headerType = this.organization?.flexibleCollections const headerType = this.i18nService.t("collections").toLowerCase();
? this.i18nService.t("collections").toLowerCase()
: this.i18nService.t("vault").toLowerCase();
if (this.collection != null) { if (this.collection != null) {
return this.collection.node.name; return this.collection.node.name;

View File

@@ -65,8 +65,8 @@
[useEvents]="organization?.canAccessEventLogs" [useEvents]="organization?.canAccessEventLogs"
[showAdminActions]="true" [showAdminActions]="true"
(onEvent)="onVaultItemsEvent($event)" (onEvent)="onVaultItemsEvent($event)"
[showBulkEditCollectionAccess]="organization?.flexibleCollections" [showBulkEditCollectionAccess]="true"
[showBulkAddToCollections]="organization?.flexibleCollections" [showBulkAddToCollections]="true"
[viewingOrgVault]="true" [viewingOrgVault]="true"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
[addAccessStatus]="addAccessStatus$ | async" [addAccessStatus]="addAccessStatus$ | async"

View File

@@ -156,7 +156,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private _flexibleCollectionsV1FlagEnabled: boolean; private _flexibleCollectionsV1FlagEnabled: boolean;
protected get flexibleCollectionsV1Enabled(): boolean { protected get flexibleCollectionsV1Enabled(): boolean {
return this._flexibleCollectionsV1FlagEnabled && this.organization?.flexibleCollections; return this._flexibleCollectionsV1FlagEnabled;
} }
protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; protected orgRevokedUsers: OrganizationUserUserDetailsResponse[];

View File

@@ -2794,12 +2794,6 @@
"userDesc": { "userDesc": {
"message": "Access and add items to assigned collections" "message": "Access and add items to assigned collections"
}, },
"manager": {
"message": "Manager"
},
"managerDesc": {
"message": "Create, delete, and manage access in assigned collections"
},
"all": { "all": {
"message": "All" "message": "All"
}, },
@@ -4576,12 +4570,6 @@
"permission": { "permission": {
"message": "Permission" "message": "Permission"
}, },
"managerPermissions": {
"message": "Manager Permissions"
},
"adminPermissions": {
"message": "Admin Permissions"
},
"accessEventLogs": { "accessEventLogs": {
"message": "Access event logs" "message": "Access event logs"
}, },
@@ -4606,9 +4594,6 @@
"deleteAnyCollection": { "deleteAnyCollection": {
"message": "Delete any collection" "message": "Delete any collection"
}, },
"manageAssignedCollections": {
"message": "Manage assigned collections"
},
"editAssignedCollections": { "editAssignedCollections": {
"message": "Edit assigned collections" "message": "Edit assigned collections"
}, },
@@ -6669,12 +6654,6 @@
"restrictedCollectionAssignmentDesc": { "restrictedCollectionAssignmentDesc": {
"message": "You can only assign collections you manage." "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": { "selectMembers": {
"message": "Select members" "message": "Select members"
}, },
@@ -6717,12 +6696,6 @@
"group": { "group": {
"message": "Group" "message": "Group"
}, },
"groupAccessAll": {
"message": "This group can access and modify all items."
},
"memberAccessAll": {
"message": "This member can access and modify all items."
},
"domainVerification": { "domainVerification": {
"message": "Domain verification" "message": "Domain verification"
}, },
@@ -8354,5 +8327,8 @@
}, },
"verified": { "verified": {
"message": "Verified" "message": "Verified"
},
"viewSecret": {
"message": "View secret"
} }
} }

View File

@@ -1,44 +1,26 @@
export class BaseAccessPolicyView { class BaseAccessPolicyView {
id: string;
read: boolean; read: boolean;
write: boolean; write: boolean;
creationDate: string;
revisionDate: string;
} }
export class UserProjectAccessPolicyView extends BaseAccessPolicyView { export class UserAccessPolicyView extends BaseAccessPolicyView {
organizationUserId: string; organizationUserId: string;
organizationUserName: string; organizationUserName: string;
grantedProjectId: string;
userId: string;
currentUser: boolean; currentUser: boolean;
} }
export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView { export class GroupAccessPolicyView extends BaseAccessPolicyView {
organizationUserId: string;
organizationUserName: string;
grantedServiceAccountId: string;
userId: string;
currentUser: boolean;
}
export class GroupProjectAccessPolicyView extends BaseAccessPolicyView {
groupId: string; groupId: string;
groupName: string; groupName: string;
grantedProjectId: string;
currentUserInGroup: boolean; currentUserInGroup: boolean;
} }
export class GroupServiceAccountAccessPolicyView extends BaseAccessPolicyView { export class ServiceAccountAccessPolicyView extends BaseAccessPolicyView {
groupId: string;
groupName: string;
grantedServiceAccountId: string;
currentUserInGroup: boolean;
}
export class ServiceAccountProjectAccessPolicyView extends BaseAccessPolicyView {
serviceAccountId: string; serviceAccountId: string;
serviceAccountName: string; serviceAccountName: string;
}
export class GrantedProjectAccessPolicyView extends BaseAccessPolicyView {
grantedProjectId: string; grantedProjectId: string;
grantedProjectName: string; grantedProjectName: string;
} }

View File

@@ -1,6 +1,6 @@
import { GroupProjectAccessPolicyView, UserProjectAccessPolicyView } from "./access-policy.view"; import { GroupAccessPolicyView, UserAccessPolicyView } from "./access-policy.view";
export class ProjectPeopleAccessPoliciesView { export class ProjectPeopleAccessPoliciesView {
userAccessPolicies: UserProjectAccessPolicyView[]; userAccessPolicies: UserAccessPolicyView[];
groupAccessPolicies: GroupProjectAccessPolicyView[]; groupAccessPolicies: GroupAccessPolicyView[];
} }

View File

@@ -1,5 +1,5 @@
import { ServiceAccountProjectAccessPolicyView } from "./access-policy.view"; import { ServiceAccountAccessPolicyView } from "./access-policy.view";
export class ProjectServiceAccountsAccessPoliciesView { export class ProjectServiceAccountsAccessPoliciesView {
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[]; serviceAccountAccessPolicies: ServiceAccountAccessPolicyView[];
} }

View File

@@ -0,0 +1,11 @@
import {
GroupAccessPolicyView,
UserAccessPolicyView,
ServiceAccountAccessPolicyView,
} from "./access-policy.view";
export class SecretAccessPoliciesView {
userAccessPolicies: UserAccessPolicyView[];
groupAccessPolicies: GroupAccessPolicyView[];
serviceAccountAccessPolicies: ServiceAccountAccessPolicyView[];
}

View File

@@ -1,10 +1,10 @@
import { ServiceAccountProjectAccessPolicyView } from "./access-policy.view"; import { GrantedProjectAccessPolicyView } from "./access-policy.view";
export class ServiceAccountGrantedPoliciesView { export class ServiceAccountGrantedPoliciesView {
grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsView[]; grantedProjectPolicies: GrantedProjectPolicyPermissionDetailsView[];
} }
export class ServiceAccountProjectPolicyPermissionDetailsView { export class GrantedProjectPolicyPermissionDetailsView {
accessPolicy: ServiceAccountProjectAccessPolicyView; accessPolicy: GrantedProjectAccessPolicyView;
hasPermission: boolean; hasPermission: boolean;
} }

View File

@@ -1,9 +1,6 @@
import { import { GroupAccessPolicyView, UserAccessPolicyView } from "./access-policy.view";
GroupServiceAccountAccessPolicyView,
UserServiceAccountAccessPolicyView,
} from "./access-policy.view";
export class ServiceAccountPeopleAccessPoliciesView { export class ServiceAccountPeopleAccessPoliciesView {
userAccessPolicies: UserServiceAccountAccessPolicyView[]; userAccessPolicies: UserAccessPolicyView[];
groupAccessPolicies: GroupServiceAccountAccessPolicyView[]; groupAccessPolicies: GroupAccessPolicyView[];
} }

View File

@@ -65,6 +65,7 @@
(deleteSecretsEvent)="openDeleteSecret($event)" (deleteSecretsEvent)="openDeleteSecret($event)"
(newSecretEvent)="openNewSecretDialog()" (newSecretEvent)="openNewSecretDialog()"
(editSecretEvent)="openEditSecret($event)" (editSecretEvent)="openEditSecret($event)"
(viewSecretEvent)="openViewSecret($event)"
(copySecretNameEvent)="copySecretName($event)" (copySecretNameEvent)="copySecretName($event)"
(copySecretValueEvent)="copySecretValue($event)" (copySecretValueEvent)="copySecretValue($event)"
(copySecretUuidEvent)="copySecretUuid($event)" (copySecretUuidEvent)="copySecretUuid($event)"

View File

@@ -41,6 +41,10 @@ import {
SecretDialogComponent, SecretDialogComponent,
SecretOperation, SecretOperation,
} from "../secrets/dialog/secret-dialog.component"; } from "../secrets/dialog/secret-dialog.component";
import {
SecretViewDialogComponent,
SecretViewDialogParams,
} from "../secrets/dialog/secret-view-dialog.component";
import { SecretService } from "../secrets/secret.service"; import { SecretService } from "../secrets/secret.service";
import { import {
ServiceAccountDialogComponent, ServiceAccountDialogComponent,
@@ -277,6 +281,15 @@ export class OverviewComponent implements OnInit, OnDestroy {
}); });
} }
openViewSecret(secretId: string) {
this.dialogService.open<unknown, SecretViewDialogParams>(SecretViewDialogComponent, {
data: {
organizationId: this.organizationId,
secretId: secretId,
},
});
}
openDeleteSecret(event: SecretListView[]) { openDeleteSecret(event: SecretListView[]) {
this.dialogService.open<unknown, SecretDeleteOperation>(SecretDeleteDialogComponent, { this.dialogService.open<unknown, SecretDeleteOperation>(SecretDeleteDialogComponent, {
data: { data: {

View File

@@ -12,7 +12,7 @@ import { DialogService } from "@bitwarden/components";
import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service"; import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service";
import { import {
ApItemValueType, ApItemValueType,
convertToProjectPeopleAccessPoliciesView, convertToPeopleAccessPoliciesView,
} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type"; } from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
import { import {
ApItemViewType, ApItemViewType,
@@ -119,10 +119,7 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy {
} }
try { try {
const projectPeopleView = convertToProjectPeopleAccessPoliciesView( const projectPeopleView = convertToPeopleAccessPoliciesView(formValues);
this.projectId,
formValues,
);
const peoplePoliciesViews = await this.accessPolicyService.putProjectPeopleAccessPolicies( const peoplePoliciesViews = await this.accessPolicyService.putProjectPeopleAccessPolicies(
this.projectId, this.projectId,
projectPeopleView, projectPeopleView,

View File

@@ -14,6 +14,7 @@
(deleteSecretsEvent)="openDeleteSecret($event)" (deleteSecretsEvent)="openDeleteSecret($event)"
(newSecretEvent)="openNewSecretDialog()" (newSecretEvent)="openNewSecretDialog()"
(editSecretEvent)="openEditSecret($event)" (editSecretEvent)="openEditSecret($event)"
(viewSecretEvent)="openViewSecret($event)"
(copySecretNameEvent)="copySecretName($event)" (copySecretNameEvent)="copySecretName($event)"
(copySecretValueEvent)="copySecretValue($event)" (copySecretValueEvent)="copySecretValue($event)"
(copySecretUuidEvent)="copySecretUuid($event)" (copySecretUuidEvent)="copySecretUuid($event)"

View File

@@ -19,6 +19,10 @@ import {
SecretDialogComponent, SecretDialogComponent,
SecretOperation, SecretOperation,
} from "../../secrets/dialog/secret-dialog.component"; } from "../../secrets/dialog/secret-dialog.component";
import {
SecretViewDialogComponent,
SecretViewDialogParams,
} from "../../secrets/dialog/secret-view-dialog.component";
import { SecretService } from "../../secrets/secret.service"; import { SecretService } from "../../secrets/secret.service";
import { SecretsListComponent } from "../../shared/secrets-list.component"; import { SecretsListComponent } from "../../shared/secrets-list.component";
import { ProjectService } from "../project.service"; import { ProjectService } from "../project.service";
@@ -88,6 +92,15 @@ export class ProjectSecretsComponent {
}); });
} }
openViewSecret(secretId: string) {
this.dialogService.open<unknown, SecretViewDialogParams>(SecretViewDialogComponent, {
data: {
organizationId: this.organizationId,
secretId: secretId,
},
});
}
openDeleteSecret(event: SecretListView[]) { openDeleteSecret(event: SecretListView[]) {
this.dialogService.open<unknown, SecretDeleteOperation>(SecretDeleteDialogComponent, { this.dialogService.open<unknown, SecretDeleteOperation>(SecretDeleteDialogComponent, {
data: { data: {

View File

@@ -144,7 +144,7 @@ export class ProjectServiceAccountsComponent implements OnInit, OnDestroy {
projectId: string, projectId: string,
selectedPolicies: ApItemValueType[], selectedPolicies: ApItemValueType[],
): Promise<ProjectServiceAccountsAccessPoliciesView> { ): Promise<ProjectServiceAccountsAccessPoliciesView> {
const view = convertToProjectServiceAccountsAccessPoliciesView(projectId, selectedPolicies); const view = convertToProjectServiceAccountsAccessPoliciesView(selectedPolicies);
return await this.accessPolicyService.putProjectServiceAccountsAccessPolicies( return await this.accessPolicyService.putProjectServiceAccountsAccessPolicies(
organizationId, organizationId,
projectId, projectId,

View File

@@ -0,0 +1,30 @@
<form [formGroup]="formGroup">
<bit-dialog
dialogSize="large"
[loading]="loading"
[title]="'viewSecret' | i18n"
[subtitle]="formGroup.get('name').value"
>
<ng-container bitDialogContent class="tw-relative">
<div class="tw-flex tw-gap-4 tw-pt-4">
<bit-form-field class="tw-w-1/3">
<bit-label>{{ "name" | i18n }}</bit-label>
<input appAutofocus formControlName="name" bitInput />
</bit-form-field>
<bit-form-field class="tw-w-full">
<bit-label>{{ "value" | i18n }}</bit-label>
<textarea bitInput rows="4" formControlName="value"></textarea>
</bit-form-field>
</div>
<bit-form-field>
<bit-label>{{ "notes" | i18n }}</bit-label>
<textarea bitInput rows="4" formControlName="notes"></textarea>
</bit-form-field>
</ng-container>
<ng-container bitDialogFooter>
<button type="button" bitButton buttonType="primary" bitFormButton bitDialogClose>
{{ "close" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@@ -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;
}
}

View File

@@ -10,6 +10,7 @@
(deleteSecretsEvent)="openDeleteSecret($event)" (deleteSecretsEvent)="openDeleteSecret($event)"
(newSecretEvent)="openNewSecretDialog()" (newSecretEvent)="openNewSecretDialog()"
(editSecretEvent)="openEditSecret($event)" (editSecretEvent)="openEditSecret($event)"
(viewSecretEvent)="openViewSecret($event)"
(copySecretNameEvent)="copySecretName($event)" (copySecretNameEvent)="copySecretName($event)"
(copySecretValueEvent)="copySecretValue($event)" (copySecretValueEvent)="copySecretValue($event)"
(copySecretUuidEvent)="copySecretUuid($event)" (copySecretUuidEvent)="copySecretUuid($event)"

View File

@@ -20,6 +20,10 @@ import {
SecretDialogComponent, SecretDialogComponent,
SecretOperation, SecretOperation,
} from "./dialog/secret-dialog.component"; } from "./dialog/secret-dialog.component";
import {
SecretViewDialogComponent,
SecretViewDialogParams,
} from "./dialog/secret-view-dialog.component";
import { SecretService } from "./secret.service"; import { SecretService } from "./secret.service";
@Component({ @Component({
@@ -77,6 +81,15 @@ export class SecretsComponent implements OnInit {
}); });
} }
openViewSecret(secretId: string) {
this.dialogService.open<unknown, SecretViewDialogParams>(SecretViewDialogComponent, {
data: {
organizationId: this.organizationId,
secretId: secretId,
},
});
}
openDeleteSecret(event: SecretListView[]) { openDeleteSecret(event: SecretListView[]) {
this.dialogService.open<unknown, SecretDeleteOperation>(SecretDeleteDialogComponent, { this.dialogService.open<unknown, SecretDeleteOperation>(SecretDeleteDialogComponent, {
data: { data: {

View File

@@ -4,12 +4,18 @@ import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
import { SecretDeleteDialogComponent } from "./dialog/secret-delete.component"; import { SecretDeleteDialogComponent } from "./dialog/secret-delete.component";
import { SecretDialogComponent } from "./dialog/secret-dialog.component"; import { SecretDialogComponent } from "./dialog/secret-dialog.component";
import { SecretViewDialogComponent } from "./dialog/secret-view-dialog.component";
import { SecretsRoutingModule } from "./secrets-routing.module"; import { SecretsRoutingModule } from "./secrets-routing.module";
import { SecretsComponent } from "./secrets.component"; import { SecretsComponent } from "./secrets.component";
@NgModule({ @NgModule({
imports: [SecretsManagerSharedModule, SecretsRoutingModule], imports: [SecretsManagerSharedModule, SecretsRoutingModule],
declarations: [SecretDeleteDialogComponent, SecretDialogComponent, SecretsComponent], declarations: [
SecretDeleteDialogComponent,
SecretDialogComponent,
SecretViewDialogComponent,
SecretsComponent,
],
providers: [], providers: [],
}) })
export class SecretsModule {} export class SecretsModule {}

View File

@@ -11,7 +11,7 @@ import { DialogService } from "@bitwarden/components";
import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service"; import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service";
import { import {
ApItemValueType, ApItemValueType,
convertToServiceAccountPeopleAccessPoliciesView, convertToPeopleAccessPoliciesView,
} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type"; } from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
import { import {
ApItemViewType, ApItemViewType,
@@ -180,10 +180,7 @@ export class ServiceAccountPeopleComponent implements OnInit, OnDestroy {
serviceAccountId: string, serviceAccountId: string,
selectedPolicies: ApItemValueType[], selectedPolicies: ApItemValueType[],
) { ) {
const serviceAccountPeopleView = convertToServiceAccountPeopleAccessPoliciesView( const serviceAccountPeopleView = convertToPeopleAccessPoliciesView(selectedPolicies);
serviceAccountId,
selectedPolicies,
);
return await this.accessPolicyService.putServiceAccountPeopleAccessPolicies( return await this.accessPolicyService.putServiceAccountPeopleAccessPolicies(
serviceAccountId, serviceAccountId,
serviceAccountPeopleView, serviceAccountPeopleView,

View File

@@ -144,10 +144,7 @@ export class ServiceAccountProjectsComponent implements OnInit, OnDestroy {
serviceAccountId: string, serviceAccountId: string,
selectedPolicies: ApItemValueType[], selectedPolicies: ApItemValueType[],
): Promise<ServiceAccountGrantedPoliciesView> { ): Promise<ServiceAccountGrantedPoliciesView> {
const grantedViews = convertToServiceAccountGrantedPoliciesView( const grantedViews = convertToServiceAccountGrantedPoliciesView(selectedPolicies);
serviceAccountId,
selectedPolicies,
);
return await this.accessPolicyService.putServiceAccountGrantedPolicies( return await this.accessPolicyService.putServiceAccountGrantedPolicies(
organizationId, organizationId,
serviceAccountId, serviceAccountId,

View File

@@ -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,
},
];
}

View File

@@ -1,17 +1,15 @@
import { import {
UserProjectAccessPolicyView, UserAccessPolicyView,
GroupProjectAccessPolicyView, GroupAccessPolicyView,
UserServiceAccountAccessPolicyView, ServiceAccountAccessPolicyView,
GroupServiceAccountAccessPolicyView, GrantedProjectAccessPolicyView,
ServiceAccountProjectAccessPolicyView,
} from "../../../../models/view/access-policies/access-policy.view"; } 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 { ProjectServiceAccountsAccessPoliciesView } from "../../../../models/view/access-policies/project-service-accounts-access-policies.view";
import { SecretAccessPoliciesView } from "../../../../models/view/access-policies/secret-access-policies.view";
import { import {
ServiceAccountGrantedPoliciesView, ServiceAccountGrantedPoliciesView,
ServiceAccountProjectPolicyPermissionDetailsView, GrantedProjectPolicyPermissionDetailsView,
} from "../../../../models/view/access-policies/service-account-granted-policies.view"; } 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 { ApItemEnum } from "./enums/ap-item.enum";
import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum"; import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum";
@@ -24,67 +22,14 @@ export type ApItemValueType = {
currentUser?: boolean; currentUser?: boolean;
}; };
export function convertToProjectPeopleAccessPoliciesView( export function convertToPeopleAccessPoliciesView(selectedPolicyValues: ApItemValueType[]) {
projectId: string, return {
selectedPolicyValues: ApItemValueType[], userAccessPolicies: convertToUserAccessPolicyViews(selectedPolicyValues),
): ProjectPeopleAccessPoliciesView { groupAccessPolicies: convertToGroupAccessPolicyViews(selectedPolicyValues),
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 convertToServiceAccountGrantedPoliciesView( export function convertToServiceAccountGrantedPoliciesView(
serviceAccountId: string,
selectedPolicyValues: ApItemValueType[], selectedPolicyValues: ApItemValueType[],
): ServiceAccountGrantedPoliciesView { ): ServiceAccountGrantedPoliciesView {
const view = new ServiceAccountGrantedPoliciesView(); const view = new ServiceAccountGrantedPoliciesView();
@@ -92,9 +37,8 @@ export function convertToServiceAccountGrantedPoliciesView(
view.grantedProjectPolicies = selectedPolicyValues view.grantedProjectPolicies = selectedPolicyValues
.filter((x) => x.type == ApItemEnum.Project) .filter((x) => x.type == ApItemEnum.Project)
.map((filtered) => { .map((filtered) => {
const detailView = new ServiceAccountProjectPolicyPermissionDetailsView(); const detailView = new GrantedProjectPolicyPermissionDetailsView();
const policyView = new ServiceAccountProjectAccessPolicyView(); const policyView = new GrantedProjectAccessPolicyView();
policyView.serviceAccountId = serviceAccountId;
policyView.grantedProjectId = filtered.id; policyView.grantedProjectId = filtered.id;
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); policyView.read = ApPermissionEnumUtil.toRead(filtered.permission);
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission);
@@ -107,21 +51,57 @@ export function convertToServiceAccountGrantedPoliciesView(
} }
export function convertToProjectServiceAccountsAccessPoliciesView( export function convertToProjectServiceAccountsAccessPoliciesView(
projectId: string,
selectedPolicyValues: ApItemValueType[], selectedPolicyValues: ApItemValueType[],
): ProjectServiceAccountsAccessPoliciesView { ): ProjectServiceAccountsAccessPoliciesView {
const view = new ProjectServiceAccountsAccessPoliciesView(); return {
serviceAccountAccessPolicies: convertToServiceAccountAccessPolicyViews(selectedPolicyValues),
};
}
view.serviceAccountAccessPolicies = selectedPolicyValues export function convertToSecretAccessPoliciesView(
.filter((x) => x.type == ApItemEnum.ServiceAccount) 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) => { .map((filtered) => {
const policyView = new ServiceAccountProjectAccessPolicyView(); const policyView = new UserAccessPolicyView();
policyView.serviceAccountId = filtered.id; policyView.organizationUserId = filtered.id;
policyView.grantedProjectId = projectId; 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.read = ApPermissionEnumUtil.toRead(filtered.permission);
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission);
return policyView; return policyView;
}); });
return view;
} }

View File

@@ -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(),
};
}

View File

@@ -1,9 +1,15 @@
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SelectItemView } from "@bitwarden/components"; 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 { PotentialGranteeView } from "../../../../models/view/access-policies/potential-grantee.view";
import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policies/project-people-access-policies.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 { 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 { 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 { 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"; import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum";
export type ApItemViewType = SelectItemView & { export type ApItemViewType = SelectItemView & {
accessPolicyId?: string;
permission?: ApPermissionEnum; permission?: ApPermissionEnum;
/** /**
* Flag that this item cannot be modified. * Flag that this item cannot be modified.
@@ -22,7 +27,6 @@ export type ApItemViewType = SelectItemView & {
} & ( } & (
| { | {
type: ApItemEnum.User; type: ApItemEnum.User;
userId?: string;
currentUser?: boolean; currentUser?: boolean;
} }
| { | {
@@ -40,38 +44,10 @@ export type ApItemViewType = SelectItemView & {
export function convertToAccessPolicyItemViews( export function convertToAccessPolicyItemViews(
value: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView, value: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView,
): ApItemViewType[] { ): ApItemViewType[] {
const accessPolicies: ApItemViewType[] = []; return [
...toUserApItemViews(value.userAccessPolicies),
value.userAccessPolicies.forEach((policy) => { ...toGroupApItemViews(value.groupAccessPolicies),
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;
} }
export function convertGrantedPoliciesToAccessPolicyItemViews( export function convertGrantedPoliciesToAccessPolicyItemViews(
@@ -84,7 +60,6 @@ export function convertGrantedPoliciesToAccessPolicyItemViews(
type: ApItemEnum.Project, type: ApItemEnum.Project,
icon: ApItemEnumUtil.itemIcon(ApItemEnum.Project), icon: ApItemEnumUtil.itemIcon(ApItemEnum.Project),
id: detailView.accessPolicy.grantedProjectId, id: detailView.accessPolicy.grantedProjectId,
accessPolicyId: detailView.accessPolicy.id,
labelName: detailView.accessPolicy.grantedProjectName, labelName: detailView.accessPolicy.grantedProjectName,
listName: detailView.accessPolicy.grantedProjectName, listName: detailView.accessPolicy.grantedProjectName,
permission: ApPermissionEnumUtil.toApPermissionEnum( permission: ApPermissionEnumUtil.toApPermissionEnum(
@@ -100,24 +75,17 @@ export function convertGrantedPoliciesToAccessPolicyItemViews(
export function convertProjectServiceAccountsViewToApItemViews( export function convertProjectServiceAccountsViewToApItemViews(
value: ProjectServiceAccountsAccessPoliciesView, value: ProjectServiceAccountsAccessPoliciesView,
): ApItemViewType[] { ): ApItemViewType[] {
const accessPolicies: ApItemViewType[] = []; return toServiceAccountsApItemViews(value.serviceAccountAccessPolicies);
}
value.serviceAccountAccessPolicies.forEach((accessPolicyView) => { export function convertSecretAccessPoliciesToApItemViews(
accessPolicies.push({ value: SecretAccessPoliciesView,
type: ApItemEnum.ServiceAccount, ): ApItemViewType[] {
icon: ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount), return [
id: accessPolicyView.serviceAccountId, ...toUserApItemViews(value.userAccessPolicies),
accessPolicyId: accessPolicyView.id, ...toGroupApItemViews(value.groupAccessPolicies),
labelName: accessPolicyView.serviceAccountName, ...toServiceAccountsApItemViews(value.serviceAccountAccessPolicies),
listName: accessPolicyView.serviceAccountName, ];
permission: ApPermissionEnumUtil.toApPermissionEnum(
accessPolicyView.read,
accessPolicyView.write,
),
readOnly: false,
});
});
return accessPolicies;
} }
export function convertPotentialGranteesToApItemViewType( 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,
};
});
}

View File

@@ -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 { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { import {
UserProjectAccessPolicyView, UserAccessPolicyView,
GroupProjectAccessPolicyView, GroupAccessPolicyView,
UserServiceAccountAccessPolicyView, ServiceAccountAccessPolicyView,
GroupServiceAccountAccessPolicyView, GrantedProjectAccessPolicyView,
ServiceAccountProjectAccessPolicyView,
} from "../../models/view/access-policies/access-policy.view"; } from "../../models/view/access-policies/access-policy.view";
import { PotentialGranteeView } from "../../models/view/access-policies/potential-grantee.view"; import { PotentialGranteeView } from "../../models/view/access-policies/potential-grantee.view";
import { ProjectPeopleAccessPoliciesView } from "../../models/view/access-policies/project-people-access-policies.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 { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policies/project-service-accounts-access-policies.view";
import { SecretAccessPoliciesView } from "../../models/view/access-policies/secret-access-policies.view";
import { import {
ServiceAccountGrantedPoliciesView, ServiceAccountGrantedPoliciesView,
ServiceAccountProjectPolicyPermissionDetailsView, GrantedProjectPolicyPermissionDetailsView,
} from "../../models/view/access-policies/service-account-granted-policies.view"; } from "../../models/view/access-policies/service-account-granted-policies.view";
import { ServiceAccountPeopleAccessPoliciesView } from "../../models/view/access-policies/service-account-people-access-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"; 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 { AccessPolicyRequest } from "./models/requests/access-policy.request";
import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request"; import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request";
import { import {
GroupServiceAccountAccessPolicyResponse, GroupAccessPolicyResponse,
UserServiceAccountAccessPolicyResponse, UserAccessPolicyResponse,
GroupProjectAccessPolicyResponse, ServiceAccountAccessPolicyResponse,
ServiceAccountProjectAccessPolicyResponse, GrantedProjectAccessPolicyResponse,
UserProjectAccessPolicyResponse,
} from "./models/responses/access-policy.response"; } from "./models/responses/access-policy.response";
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response"; import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response";
import { ProjectServiceAccountsAccessPoliciesResponse } from "./models/responses/project-service-accounts-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 { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response";
import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.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({ @Injectable({
providedIn: "root", providedIn: "root",
@@ -63,7 +63,7 @@ export class AccessPolicyService {
); );
const results = new ProjectPeopleAccessPoliciesResponse(r); const results = new ProjectPeopleAccessPoliciesResponse(r);
return this.createProjectPeopleAccessPoliciesView(results); return this.createPeopleAccessPoliciesView(results);
} }
async putProjectPeopleAccessPolicies( async putProjectPeopleAccessPolicies(
@@ -79,7 +79,7 @@ export class AccessPolicyService {
true, true,
); );
const results = new ProjectPeopleAccessPoliciesResponse(r); const results = new ProjectPeopleAccessPoliciesResponse(r);
return this.createProjectPeopleAccessPoliciesView(results); return this.createPeopleAccessPoliciesView(results);
} }
async getServiceAccountPeopleAccessPolicies( async getServiceAccountPeopleAccessPolicies(
@@ -94,7 +94,7 @@ export class AccessPolicyService {
); );
const results = new ServiceAccountPeopleAccessPoliciesResponse(r); const results = new ServiceAccountPeopleAccessPoliciesResponse(r);
return this.createServiceAccountPeopleAccessPoliciesView(results); return this.createPeopleAccessPoliciesView(results);
} }
async putServiceAccountPeopleAccessPolicies( async putServiceAccountPeopleAccessPolicies(
@@ -110,7 +110,7 @@ export class AccessPolicyService {
true, true,
); );
const results = new ServiceAccountPeopleAccessPoliciesResponse(r); const results = new ServiceAccountPeopleAccessPoliciesResponse(r);
return this.createServiceAccountPeopleAccessPoliciesView(results); return this.createPeopleAccessPoliciesView(results);
} }
async getServiceAccountGrantedPolicies( async getServiceAccountGrantedPolicies(
@@ -181,6 +181,22 @@ export class AccessPolicyService {
return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId); return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId);
} }
async getSecretAccessPolicies(
organizationId: string,
secretId: string,
): Promise<SecretAccessPoliciesView> {
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) { async getPeoplePotentialGrantees(organizationId: string) {
const r = await this.apiService.send( const r = await this.apiService.send(
"GET", "GET",
@@ -223,12 +239,7 @@ export class AccessPolicyService {
private getAccessPolicyRequest( private getAccessPolicyRequest(
granteeId: string, granteeId: string,
view: view: UserAccessPolicyView | GroupAccessPolicyView | ServiceAccountAccessPolicyView,
| UserProjectAccessPolicyView
| UserServiceAccountAccessPolicyView
| GroupProjectAccessPolicyView
| GroupServiceAccountAccessPolicyView
| ServiceAccountProjectAccessPolicyView,
) { ) {
const request = new AccessPolicyRequest(); const request = new AccessPolicyRequest();
request.granteeId = granteeId; request.granteeId = granteeId;
@@ -285,21 +296,79 @@ export class AccessPolicyService {
private createBaseAccessPolicyView( private createBaseAccessPolicyView(
response: response:
| UserProjectAccessPolicyResponse | UserAccessPolicyResponse
| UserServiceAccountAccessPolicyResponse | GroupAccessPolicyResponse
| GroupProjectAccessPolicyResponse | ServiceAccountAccessPolicyResponse
| GroupServiceAccountAccessPolicyResponse | GrantedProjectAccessPolicyResponse,
| ServiceAccountProjectAccessPolicyResponse,
) { ) {
return { return {
id: response.id,
read: response.read, read: response.read,
write: response.write, write: response.write,
creationDate: response.creationDate,
revisionDate: response.revisionDate,
}; };
} }
private async createGrantedProjectAccessPolicyView(
organizationKey: SymmetricCryptoKey,
response: GrantedProjectAccessPolicyResponse,
): Promise<GrantedProjectAccessPolicyView> {
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<ServiceAccountAccessPolicyView[]> {
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( private async createPotentialGranteeViews(
organizationId: string, organizationId: string,
results: PotentialGranteeResponse[], results: PotentialGranteeResponse[],
@@ -332,137 +401,44 @@ export class AccessPolicyService {
): Promise<ServiceAccountGrantedPoliciesView> { ): Promise<ServiceAccountGrantedPoliciesView> {
const orgKey = await this.getOrganizationKey(organizationId); const orgKey = await this.getOrganizationKey(organizationId);
const view = new ServiceAccountGrantedPoliciesView(); return {
view.grantedProjectPolicies = grantedProjectPolicies: await this.createGrantedProjectPolicyPermissionDetailsViews(
await this.createServiceAccountProjectPolicyPermissionDetailsViews(
orgKey, orgKey,
response.grantedProjectPolicies, response.grantedProjectPolicies,
); ),
return view; };
} }
private async createServiceAccountProjectPolicyPermissionDetailsViews( private async createGrantedProjectPolicyPermissionDetailsViews(
orgKey: SymmetricCryptoKey, orgKey: SymmetricCryptoKey,
responses: ServiceAccountProjectPolicyPermissionDetailsResponse[], responses: GrantedProjectAccessPolicyPermissionDetailsResponse[],
): Promise<ServiceAccountProjectPolicyPermissionDetailsView[]> { ): Promise<GrantedProjectPolicyPermissionDetailsView[]> {
return await Promise.all( return await Promise.all(
responses.map(async (response) => { responses.map(async (response) => {
return await this.createServiceAccountProjectPolicyPermissionDetailsView(orgKey, response); return await this.createGrantedProjectPolicyPermissionDetailsView(orgKey, response);
}), }),
); );
} }
private async createServiceAccountProjectPolicyPermissionDetailsView( private async createGrantedProjectPolicyPermissionDetailsView(
orgKey: SymmetricCryptoKey, orgKey: SymmetricCryptoKey,
response: ServiceAccountProjectPolicyPermissionDetailsResponse, response: GrantedProjectAccessPolicyPermissionDetailsResponse,
): Promise<ServiceAccountProjectPolicyPermissionDetailsView> { ): Promise<GrantedProjectPolicyPermissionDetailsView> {
const view = new ServiceAccountProjectPolicyPermissionDetailsView(); const view = new GrantedProjectPolicyPermissionDetailsView();
view.hasPermission = response.hasPermission; view.hasPermission = response.hasPermission;
view.accessPolicy = await this.createServiceAccountProjectAccessPolicyView( view.accessPolicy = await this.createGrantedProjectAccessPolicyView(
orgKey, orgKey,
response.accessPolicy, response.accessPolicy,
); );
return view; return view;
} }
private createProjectPeopleAccessPoliciesView( private createPeopleAccessPoliciesView(
peopleAccessPoliciesResponse: ProjectPeopleAccessPoliciesResponse, response: ProjectPeopleAccessPoliciesResponse | ServiceAccountPeopleAccessPoliciesResponse,
): 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 {
return { return {
...this.createBaseAccessPolicyView(response), userAccessPolicies: this.createUserAccessPolicyViews(response.userAccessPolicies),
grantedProjectId: response.grantedProjectId, groupAccessPolicies: this.createGroupAccessPolicyViews(response.groupAccessPolicies),
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<ServiceAccountProjectAccessPolicyView> {
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,
}; };
} }
@@ -471,13 +447,26 @@ export class AccessPolicyService {
organizationId: string, organizationId: string,
): Promise<ProjectServiceAccountsAccessPoliciesView> { ): Promise<ProjectServiceAccountsAccessPoliciesView> {
const orgKey = await this.getOrganizationKey(organizationId); const orgKey = await this.getOrganizationKey(organizationId);
return {
serviceAccountAccessPolicies: await this.createServiceAccountAccessPolicyViews(
orgKey,
response.serviceAccountAccessPolicies,
),
};
}
const view = new ProjectServiceAccountsAccessPoliciesView(); private async createSecretAccessPoliciesView(
view.serviceAccountAccessPolicies = await Promise.all( response: SecretAccessPoliciesResponse,
response.serviceAccountAccessPolicies.map(async (ap) => { organizationId: string,
return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap); ): Promise<SecretAccessPoliciesView> {
}), const orgKey = await this.getOrganizationKey(organizationId);
); return {
return view; userAccessPolicies: this.createUserAccessPolicyViews(response.userAccessPolicies),
groupAccessPolicies: this.createGroupAccessPolicyViews(response.groupAccessPolicies),
serviceAccountAccessPolicies: await this.createServiceAccountAccessPolicyViews(
orgKey,
response.serviceAccountAccessPolicies,
),
};
} }
} }

View File

@@ -1,96 +1,59 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class BaseAccessPolicyResponse extends BaseResponse { class BaseAccessPolicyResponse extends BaseResponse {
id: string;
read: boolean; read: boolean;
write: boolean; write: boolean;
creationDate: string;
revisionDate: string;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.id = this.getResponseProperty("Id");
this.read = this.getResponseProperty("Read"); this.read = this.getResponseProperty("Read");
this.write = this.getResponseProperty("Write"); 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; organizationUserId: string;
organizationUserName: string; organizationUserName: string;
grantedProjectId: string;
userId: string;
currentUser: boolean; currentUser: boolean;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.organizationUserId = this.getResponseProperty("OrganizationUserId"); this.organizationUserId = this.getResponseProperty("OrganizationUserId");
this.organizationUserName = this.getResponseProperty("OrganizationUserName"); this.organizationUserName = this.getResponseProperty("OrganizationUserName");
this.grantedProjectId = this.getResponseProperty("GrantedProjectId");
this.userId = this.getResponseProperty("UserId");
this.currentUser = this.getResponseProperty("CurrentUser"); this.currentUser = this.getResponseProperty("CurrentUser");
} }
} }
export class UserServiceAccountAccessPolicyResponse extends BaseAccessPolicyResponse { export class GroupAccessPolicyResponse 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 {
groupId: string; groupId: string;
groupName: string; groupName: string;
grantedProjectId: string;
currentUserInGroup: boolean; currentUserInGroup: boolean;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.groupId = this.getResponseProperty("GroupId"); this.groupId = this.getResponseProperty("GroupId");
this.groupName = this.getResponseProperty("GroupName"); this.groupName = this.getResponseProperty("GroupName");
this.grantedProjectId = this.getResponseProperty("GrantedProjectId");
this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup"); this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup");
} }
} }
export class GroupServiceAccountAccessPolicyResponse extends BaseAccessPolicyResponse { export class ServiceAccountAccessPolicyResponse 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 {
serviceAccountId: string; serviceAccountId: string;
serviceAccountName: string; serviceAccountName: string;
grantedProjectId: string;
grantedProjectName: string;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
this.serviceAccountId = this.getResponseProperty("ServiceAccountId"); this.serviceAccountId = this.getResponseProperty("ServiceAccountId");
this.serviceAccountName = this.getResponseProperty("ServiceAccountName"); 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.grantedProjectId = this.getResponseProperty("GrantedProjectId");
this.grantedProjectName = this.getResponseProperty("GrantedProjectName"); this.grantedProjectName = this.getResponseProperty("GrantedProjectName");
} }

View File

@@ -1,23 +1,18 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { import { GroupAccessPolicyResponse, UserAccessPolicyResponse } from "./access-policy.response";
GroupProjectAccessPolicyResponse,
UserProjectAccessPolicyResponse,
} from "./access-policy.response";
export class ProjectPeopleAccessPoliciesResponse extends BaseResponse { export class ProjectPeopleAccessPoliciesResponse extends BaseResponse {
userAccessPolicies: UserProjectAccessPolicyResponse[]; userAccessPolicies: UserAccessPolicyResponse[];
groupAccessPolicies: GroupProjectAccessPolicyResponse[]; groupAccessPolicies: GroupAccessPolicyResponse[];
constructor(response: any) { constructor(response: any) {
super(response); super(response);
const userAccessPolicies = this.getResponseProperty("UserAccessPolicies"); const userAccessPolicies = this.getResponseProperty("UserAccessPolicies");
this.userAccessPolicies = userAccessPolicies.map( this.userAccessPolicies = userAccessPolicies.map((k: any) => new UserAccessPolicyResponse(k));
(k: any) => new UserProjectAccessPolicyResponse(k),
);
const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies"); const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies");
this.groupAccessPolicies = groupAccessPolicies.map( this.groupAccessPolicies = groupAccessPolicies.map(
(k: any) => new GroupProjectAccessPolicyResponse(k), (k: any) => new GroupAccessPolicyResponse(k),
); );
} }
} }

View File

@@ -1,15 +1,15 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; 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 { export class ProjectServiceAccountsAccessPoliciesResponse extends BaseResponse {
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[]; serviceAccountAccessPolicies: ServiceAccountAccessPolicyResponse[];
constructor(response: any) { constructor(response: any) {
super(response); super(response);
const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies"); const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies");
this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map( this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map(
(k: any) => new ServiceAccountProjectAccessPolicyResponse(k), (k: any) => new ServiceAccountAccessPolicyResponse(k),
); );
} }
} }

View File

@@ -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),
);
}
}

View File

@@ -1,15 +1,15 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; 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 { export class ServiceAccountGrantedPoliciesPermissionDetailsResponse extends BaseResponse {
grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsResponse[]; grantedProjectPolicies: GrantedProjectAccessPolicyPermissionDetailsResponse[];
constructor(response: any) { constructor(response: any) {
super(response); super(response);
const grantedProjectPolicies = this.getResponseProperty("GrantedProjectPolicies"); const grantedProjectPolicies = this.getResponseProperty("GrantedProjectPolicies");
this.grantedProjectPolicies = grantedProjectPolicies.map( this.grantedProjectPolicies = grantedProjectPolicies.map(
(k: any) => new ServiceAccountProjectPolicyPermissionDetailsResponse(k), (k: any) => new GrantedProjectAccessPolicyPermissionDetailsResponse(k),
); );
} }
} }

View File

@@ -1,23 +1,18 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { import { GroupAccessPolicyResponse, UserAccessPolicyResponse } from "./access-policy.response";
GroupServiceAccountAccessPolicyResponse,
UserServiceAccountAccessPolicyResponse,
} from "./access-policy.response";
export class ServiceAccountPeopleAccessPoliciesResponse extends BaseResponse { export class ServiceAccountPeopleAccessPoliciesResponse extends BaseResponse {
userAccessPolicies: UserServiceAccountAccessPolicyResponse[]; userAccessPolicies: UserAccessPolicyResponse[];
groupAccessPolicies: GroupServiceAccountAccessPolicyResponse[]; groupAccessPolicies: GroupAccessPolicyResponse[];
constructor(response: any) { constructor(response: any) {
super(response); super(response);
const userAccessPolicies = this.getResponseProperty("UserAccessPolicies"); const userAccessPolicies = this.getResponseProperty("UserAccessPolicies");
this.userAccessPolicies = userAccessPolicies.map( this.userAccessPolicies = userAccessPolicies.map((k: any) => new UserAccessPolicyResponse(k));
(k: any) => new UserServiceAccountAccessPolicyResponse(k),
);
const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies"); const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies");
this.groupAccessPolicies = groupAccessPolicies.map( this.groupAccessPolicies = groupAccessPolicies.map(
(k: any) => new GroupServiceAccountAccessPolicyResponse(k), (k: any) => new GroupAccessPolicyResponse(k),
); );
} }
} }

View File

@@ -1,9 +1,9 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response"; 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 { export class GrantedProjectAccessPolicyPermissionDetailsResponse extends BaseResponse {
accessPolicy: ServiceAccountProjectAccessPolicyResponse; accessPolicy: GrantedProjectAccessPolicyResponse;
hasPermission: boolean; hasPermission: boolean;
constructor(response: any) { constructor(response: any) {

View File

@@ -66,7 +66,7 @@
<i class="bwi bwi-key tw-text-muted" aria-hidden="true"></i> <i class="bwi bwi-key tw-text-muted" aria-hidden="true"></i>
<div> <div>
<div *ngIf="!trash"> <div *ngIf="!trash">
<button type="button" bitLink (click)="editSecretEvent.emit(secret.id)"> <button type="button" bitLink (click)="editSecret(secret)">
{{ secret.name }} {{ secret.name }}
</button> </button>
</div> </div>
@@ -118,7 +118,7 @@
<button <button
type="button" type="button"
bitMenuItem bitMenuItem
(click)="editSecretEvent.emit(secret.id)" (click)="editSecret(secret)"
*ngIf="secret.write && !trash" *ngIf="secret.write && !trash"
> >
<i class="bwi bwi-fw bwi-pencil" aria-hidden="true"></i> <i class="bwi bwi-fw bwi-pencil" aria-hidden="true"></i>

View File

@@ -37,6 +37,7 @@ export class SecretsListComponent implements OnDestroy {
@Input() trash: boolean; @Input() trash: boolean;
@Output() editSecretEvent = new EventEmitter<string>(); @Output() editSecretEvent = new EventEmitter<string>();
@Output() viewSecretEvent = new EventEmitter<string>();
@Output() copySecretNameEvent = new EventEmitter<string>(); @Output() copySecretNameEvent = new EventEmitter<string>();
@Output() copySecretValueEvent = new EventEmitter<string>(); @Output() copySecretValueEvent = new EventEmitter<string>();
@Output() copySecretUuidEvent = new EventEmitter<string>(); @Output() copySecretUuidEvent = new EventEmitter<string>();
@@ -116,6 +117,14 @@ export class SecretsListComponent implements OnDestroy {
return aProjects[0]?.name.localeCompare(bProjects[0].name); return aProjects[0]?.name.localeCompare(bProjects[0].name);
}; };
protected editSecret(secret: SecretListView) {
if (secret.write) {
this.editSecretEvent.emit(secret.id);
} else {
this.viewSecretEvent.emit(secret.id);
}
}
/** /**
* TODO: Refactor to smart component and remove * TODO: Refactor to smart component and remove
*/ */

View File

@@ -20,8 +20,6 @@ export class UserTypePipe implements PipeTransform {
return this.i18nService.t("admin"); return this.i18nService.t("admin");
case OrganizationUserType.User: case OrganizationUserType.User:
return this.i18nService.t("user"); return this.i18nService.t("user");
case OrganizationUserType.Manager:
return this.i18nService.t("manager");
case OrganizationUserType.Custom: case OrganizationUserType.Custom:
return this.i18nService.t("custom"); return this.i18nService.t("custom");
} }

View File

@@ -1,4 +1,4 @@
import { Observable, Subject, Subscription, firstValueFrom, throwError, timeout } from "rxjs"; import { firstValueFrom, Observable, Subject, Subscription, throwError, timeout } from "rxjs";
/** Test class to enable async awaiting of observable emissions */ /** Test class to enable async awaiting of observable emissions */
export class ObservableTracker<T> { export class ObservableTracker<T> {
@@ -43,6 +43,9 @@ export class ObservableTracker<T> {
private trackEmissions(observable: Observable<T>): T[] { private trackEmissions(observable: Observable<T>): T[] {
const emissions: T[] = []; const emissions: T[] = [];
this.emissionReceived.subscribe((value) => {
emissions.push(value);
});
this.subscription = observable.subscribe((value) => { this.subscription = observable.subscribe((value) => {
if (value == null) { if (value == null) {
this.emissionReceived.next(null); this.emissionReceived.next(null);
@@ -64,9 +67,7 @@ export class ObservableTracker<T> {
} }
} }
}); });
this.emissionReceived.subscribe((value) => {
emissions.push(value);
});
return emissions; return emissions;
} }
} }

View File

@@ -5,7 +5,6 @@ import { SelectionReadOnlyRequest } from "../../../models/request/selection-read
export class OrganizationUserInviteRequest { export class OrganizationUserInviteRequest {
emails: string[] = []; emails: string[] = [];
type: OrganizationUserType; type: OrganizationUserType;
accessAll: boolean;
accessSecretsManager: boolean; accessSecretsManager: boolean;
collections: SelectionReadOnlyRequest[] = []; collections: SelectionReadOnlyRequest[] = [];
groups: string[]; groups: string[];

View File

@@ -4,7 +4,6 @@ import { SelectionReadOnlyRequest } from "../../../models/request/selection-read
export class OrganizationUserUpdateRequest { export class OrganizationUserUpdateRequest {
type: OrganizationUserType; type: OrganizationUserType;
accessAll: boolean;
accessSecretsManager: boolean; accessSecretsManager: boolean;
collections: SelectionReadOnlyRequest[] = []; collections: SelectionReadOnlyRequest[] = [];
groups: string[] = []; groups: string[] = [];

View File

@@ -10,11 +10,6 @@ export class OrganizationUserResponse extends BaseResponse {
type: OrganizationUserType; type: OrganizationUserType;
status: OrganizationUserStatusType; status: OrganizationUserStatusType;
externalId: string; externalId: string;
/**
* @deprecated
* To be removed after Flexible Collections.
**/
accessAll: boolean;
accessSecretsManager: boolean; accessSecretsManager: boolean;
permissions: PermissionsApi; permissions: PermissionsApi;
resetPasswordEnrolled: boolean; resetPasswordEnrolled: boolean;
@@ -30,7 +25,6 @@ export class OrganizationUserResponse extends BaseResponse {
this.status = this.getResponseProperty("Status"); this.status = this.getResponseProperty("Status");
this.permissions = new PermissionsApi(this.getResponseProperty("Permissions")); this.permissions = new PermissionsApi(this.getResponseProperty("Permissions"));
this.externalId = this.getResponseProperty("ExternalId"); this.externalId = this.getResponseProperty("ExternalId");
this.accessAll = this.getResponseProperty("AccessAll");
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager"); this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled"); this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword"); this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");

View File

@@ -7,7 +7,7 @@ import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization"; import { Organization } from "../../models/domain/organization";
export function canAccessVaultTab(org: Organization): boolean { export function canAccessVaultTab(org: Organization): boolean {
return org.canViewAssignedCollections || org.canViewAllCollections; return org.canViewAllCollections;
} }
export function canAccessSettingsTab(org: Organization): boolean { export function canAccessSettingsTab(org: Organization): boolean {
@@ -77,10 +77,7 @@ export function canAccessImportExport(i18nService: I18nService) {
export function canAccessImport(i18nService: I18nService) { export function canAccessImport(i18nService: I18nService) {
return map<Organization[], Organization[]>((orgs) => return map<Organization[], Organization[]>((orgs) =>
orgs orgs
.filter( .filter((org) => org.canAccessImportExport || org.canCreateNewCollections)
(org) =>
org.canAccessImportExport || (org.canCreateNewCollections && org.flexibleCollections),
)
.sort(Utils.getSortFunction(i18nService, "name")), .sort(Utils.getSortFunction(i18nService, "name")),
); );
} }

View File

@@ -39,6 +39,7 @@ describe("ORGANIZATIONS state", () => {
permissions: undefined, permissions: undefined,
resetPasswordEnrolled: false, resetPasswordEnrolled: false,
userId: "userId", userId: "userId",
organizationUserId: "organizationUserId",
hasPublicAndPrivateKeys: false, hasPublicAndPrivateKeys: false,
providerId: "providerId", providerId: "providerId",
providerName: "providerName", providerName: "providerName",

View File

@@ -36,6 +36,7 @@ export class OrganizationData {
permissions: PermissionsApi; permissions: PermissionsApi;
resetPasswordEnrolled: boolean; resetPasswordEnrolled: boolean;
userId: string; userId: string;
organizationUserId: string;
hasPublicAndPrivateKeys: boolean; hasPublicAndPrivateKeys: boolean;
providerId: string; providerId: string;
providerName: string; providerName: string;
@@ -96,6 +97,7 @@ export class OrganizationData {
this.permissions = response.permissions; this.permissions = response.permissions;
this.resetPasswordEnrolled = response.resetPasswordEnrolled; this.resetPasswordEnrolled = response.resetPasswordEnrolled;
this.userId = response.userId; this.userId = response.userId;
this.organizationUserId = response.organizationUserId;
this.hasPublicAndPrivateKeys = response.hasPublicAndPrivateKeys; this.hasPublicAndPrivateKeys = response.hasPublicAndPrivateKeys;
this.providerId = response.providerId; this.providerId = response.providerId;
this.providerName = response.providerName; this.providerName = response.providerName;

View File

@@ -43,6 +43,7 @@ export class Organization {
permissions: PermissionsApi; permissions: PermissionsApi;
resetPasswordEnrolled: boolean; resetPasswordEnrolled: boolean;
userId: string; userId: string;
organizationUserId: string;
hasPublicAndPrivateKeys: boolean; hasPublicAndPrivateKeys: boolean;
providerId: string; providerId: string;
providerName: string; providerName: string;
@@ -113,6 +114,7 @@ export class Organization {
this.permissions = obj.permissions; this.permissions = obj.permissions;
this.resetPasswordEnrolled = obj.resetPasswordEnrolled; this.resetPasswordEnrolled = obj.resetPasswordEnrolled;
this.userId = obj.userId; this.userId = obj.userId;
this.organizationUserId = obj.organizationUserId;
this.hasPublicAndPrivateKeys = obj.hasPublicAndPrivateKeys; this.hasPublicAndPrivateKeys = obj.hasPublicAndPrivateKeys;
this.providerId = obj.providerId; this.providerId = obj.providerId;
this.providerName = obj.providerName; this.providerName = obj.providerName;
@@ -140,16 +142,6 @@ export class Organization {
return this.enabled && this.status === OrganizationUserStatusType.Confirmed; return this.enabled && this.status === OrganizationUserStatusType.Confirmed;
} }
/**
* Whether a user has Manager permissions or greater
*
* @deprecated
* This is deprecated with the introduction of Flexible Collections.
*/
get isManager() {
return this.type === OrganizationUserType.Manager || this.isAdmin;
}
/** /**
* Whether a user has Admin permissions or greater * Whether a user has Admin permissions or greater
*/ */
@@ -177,19 +169,13 @@ export class Organization {
} }
get canCreateNewCollections() { get canCreateNewCollections() {
if (this.flexibleCollections) { return (
return ( !this.limitCollectionCreationDeletion || this.isAdmin || this.permissions.createNewCollections
!this.limitCollectionCreationDeletion || );
this.isAdmin ||
this.permissions.createNewCollections
);
}
return this.isManager || this.permissions.createNewCollections;
} }
canEditAnyCollection(flexibleCollectionsV1Enabled: boolean) { canEditAnyCollection(flexibleCollectionsV1Enabled: boolean) {
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) { if (!flexibleCollectionsV1Enabled) {
// Pre-Flexible Collections v1 logic // Pre-Flexible Collections v1 logic
return this.isAdmin || this.permissions.editAnyCollection; return this.isAdmin || this.permissions.editAnyCollection;
} }
@@ -219,8 +205,8 @@ export class Organization {
flexibleCollectionsV1Enabled: boolean, flexibleCollectionsV1Enabled: boolean,
restrictProviderAccessFlagEnabled: boolean, restrictProviderAccessFlagEnabled: boolean,
) { ) {
// Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers // Before Flexible Collections V1, any admin or anyone with editAnyCollection permission could edit all ciphers
if (!this.flexibleCollections || !flexibleCollectionsV1Enabled || !this.flexibleCollections) { if (!flexibleCollectionsV1Enabled) {
return this.isAdmin || this.permissions.editAnyCollection; return this.isAdmin || this.permissions.editAnyCollection;
} }
@@ -267,33 +253,6 @@ export class Organization {
); );
} }
/**
* @deprecated
* This is deprecated with the introduction of Flexible Collections.
* This will always return false if FlexibleCollections flag is on.
*/
get canEditAssignedCollections() {
return this.isManager || this.permissions.editAssignedCollections;
}
/**
* @deprecated
* This is deprecated with the introduction of Flexible Collections.
* This will always return false if FlexibleCollections flag is on.
*/
get canDeleteAssignedCollections() {
return this.isManager || this.permissions.deleteAssignedCollections;
}
/**
* @deprecated
* This is deprecated with the introduction of Flexible Collections.
* This will always return false if FlexibleCollections flag is on.
*/
get canViewAssignedCollections() {
return this.canDeleteAssignedCollections || this.canEditAssignedCollections;
}
get canManageGroups() { get canManageGroups() {
return (this.isAdmin || this.permissions.manageGroups) && this.useGroups; return (this.isAdmin || this.permissions.manageGroups) && this.useGroups;
} }

View File

@@ -36,6 +36,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
permissions: PermissionsApi; permissions: PermissionsApi;
resetPasswordEnrolled: boolean; resetPasswordEnrolled: boolean;
userId: string; userId: string;
organizationUserId: string;
providerId: string; providerId: string;
providerName: string; providerName: string;
providerType?: ProviderType; providerType?: ProviderType;
@@ -86,6 +87,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.permissions = new PermissionsApi(this.getResponseProperty("permissions")); this.permissions = new PermissionsApi(this.getResponseProperty("permissions"));
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled"); this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
this.userId = this.getResponseProperty("UserId"); this.userId = this.getResponseProperty("UserId");
this.organizationUserId = this.getResponseProperty("OrganizationUserId");
this.providerId = this.getResponseProperty("ProviderId"); this.providerId = this.getResponseProperty("ProviderId");
this.providerName = this.getResponseProperty("ProviderName"); this.providerName = this.getResponseProperty("ProviderName");
this.providerType = this.getResponseProperty("ProviderType"); this.providerType = this.getResponseProperty("ProviderType");

View File

@@ -49,15 +49,11 @@ export class CollectionView implements View, ITreeNodeObject {
); );
} }
if (org?.flexibleCollections) { return (
return ( org?.canEditAllCiphers(v1FlexibleCollections, restrictProviderAccess) ||
org?.canEditAllCiphers(v1FlexibleCollections, restrictProviderAccess) || this.manage ||
this.manage || (this.assigned && !this.readOnly)
(this.assigned && !this.readOnly) );
);
}
return org?.canEditAnyCollection(false) || (org?.canEditAssignedCollections && this.assigned);
} }
/** /**

View File

@@ -256,17 +256,14 @@ export class ImportComponent implements OnInit, OnDestroy {
if (!this._importBlockedByPolicy) { if (!this._importBlockedByPolicy) {
this.formGroup.controls.targetSelector.enable(); this.formGroup.controls.targetSelector.enable();
} }
const flexCollectionEnabled =
organizations.find((x) => x.id == this.organizationId)?.flexibleCollections ?? false;
if (value) { if (value) {
this.collections$ = Utils.asyncToObservable(() => this.collections$ = Utils.asyncToObservable(() =>
this.collectionService this.collectionService
.getAllDecrypted() .getAllDecrypted()
.then((decryptedCollections) => .then((decryptedCollections) =>
decryptedCollections decryptedCollections
.filter( .filter((c2) => c2.organizationId === value && c2.manage)
(c2) => c2.organizationId === value && (!flexCollectionEnabled || c2.manage),
)
.sort(Utils.getSortFunction(this.i18nService, "name")), .sort(Utils.getSortFunction(this.i18nService, "name")),
), ),
); );

Some files were not shown because too many files have changed in this diff Show More