1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

Merge branch 'main' into autofill/pm-8518-autofill-scripts-do-not-inject-into-sub-frames-on-install

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

View File

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

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

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

View File

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

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

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", () => {
it("should call search Service with the new search term", (done) => {
const searchText = "Hello";

View File

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

View File

@@ -3,6 +3,8 @@ import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, skipWhile } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ProductType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -23,6 +25,7 @@ describe("VaultPopupListFiltersService", () => {
const folderViews$ = new BehaviorSubject([]);
const cipherViews$ = new BehaviorSubject({});
const decryptedCollections$ = new BehaviorSubject<CollectionView[]>([]);
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(false);
const collectionService = {
decryptedCollections$,
@@ -45,9 +48,15 @@ describe("VaultPopupListFiltersService", () => {
t: (key: string) => key,
} as I18nService;
const policyService = {
policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$),
};
beforeEach(() => {
memberOrganizations$.next([]);
decryptedCollections$.next([]);
policyAppliesToActiveUser$.next(false);
policyService.policyAppliesToActiveUser$.mockClear();
collectionService.getAllNested = () => Promise.resolve([]);
TestBed.configureTestingModule({
@@ -72,6 +81,10 @@ describe("VaultPopupListFiltersService", () => {
provide: CollectionService,
useValue: collectionService,
},
{
provide: PolicyService,
useValue: policyService,
},
{ provide: FormBuilder, useClass: FormBuilder },
],
});
@@ -127,6 +140,65 @@ describe("VaultPopupListFiltersService", () => {
});
});
describe("PersonalOwnership policy", () => {
it('calls policyAppliesToActiveUser$ with "PersonalOwnership"', () => {
expect(policyService.policyAppliesToActiveUser$).toHaveBeenCalledWith(
PolicyType.PersonalOwnership,
);
});
it("returns an empty array when the policy applies and there is a single organization", (done) => {
policyAppliesToActiveUser$.next(true);
memberOrganizations$.next([
{ name: "bobby's org", id: "1234-3323-23223" },
] as Organization[]);
service.organizations$.subscribe((organizations) => {
expect(organizations).toEqual([]);
done();
});
});
it('adds "myVault" when the policy does not apply and there are multiple organizations', (done) => {
policyAppliesToActiveUser$.next(false);
const orgs = [
{ name: "bobby's org", id: "1234-3323-23223" },
{ name: "alice's org", id: "2223-4343-99888" },
] as Organization[];
memberOrganizations$.next(orgs);
service.organizations$.subscribe((organizations) => {
expect(organizations.map((o) => o.label)).toEqual([
"myVault",
"alice's org",
"bobby's org",
]);
done();
});
});
it('does not add "myVault" the policy applies and there are multiple organizations', (done) => {
policyAppliesToActiveUser$.next(true);
const orgs = [
{ name: "bobby's org", id: "1234-3323-23223" },
{ name: "alice's org", id: "2223-3242-99888" },
{ name: "catherine's org", id: "77733-4343-99888" },
] as Organization[];
memberOrganizations$.next(orgs);
service.organizations$.subscribe((organizations) => {
expect(organizations.map((o) => o.label)).toEqual([
"alice's org",
"bobby's org",
"catherine's org",
]);
done();
});
});
});
describe("icons", () => {
it("sets family icon for family organizations", (done) => {
const orgs = [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,14 +49,6 @@
<bit-label>{{ "user" | i18n }}</bit-label>
<bit-hint>{{ "userDesc" | i18n }}</bit-hint>
</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-label>{{ "admin" | i18n }}</bit-label>
<bit-hint>{{ "adminDesc" | i18n }}</bit-hint>
@@ -91,140 +83,64 @@
</bit-radio-button>
</bit-radio-group>
<ng-container *ngIf="customUserTypeSelected">
<ng-container *ngIf="!organization.flexibleCollections; else customPermissionsFC">
<h3 bitTypography="h3">
{{ "permissions" | i18n }}
</h3>
<div class="tw-grid tw-grid-cols-12 tw-gap-4" [formGroup]="permissionsGroup">
<div class="tw-col-span-6">
<div class="tw-mb-3">
<bit-label class="tw-font-semibold">{{
"managerPermissions" | i18n
}}</bit-label>
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
<app-nested-checkbox
parentId="manageAssignedCollections"
[checkboxes]="permissionsGroup.controls.manageAssignedCollectionsGroup"
>
</app-nested-checkbox>
</div>
</div>
<div class="tw-col-span-6">
<div class="tw-mb-3">
<bit-label class="tw-font-semibold">{{ "adminPermissions" | i18n }}</bit-label>
<hr class="tw-mb-2 tw-mr-2 tw-mt-0" />
<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>
<app-nested-checkbox
parentId="manageAllCollections"
[checkboxes]="permissionsGroup.controls.manageAllCollectionsGroup"
>
</app-nested-checkbox>
<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 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>
</ng-container>
<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>
</div>
</ng-container>
<ng-container *ngIf="organization.useSecretsManager">
<h3 class="tw-mt-4">
@@ -272,7 +188,6 @@
[columnHeader]="'groups' | i18n"
[selectorLabelText]="'selectGroups' | i18n"
[emptySelectionText]="'noGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections"
[hideMultiSelect]="restrictEditingSelf$ | async"
></bit-access-selector>
</bit-tab>
@@ -294,26 +209,7 @@
{{ "restrictedCollectionAssignmentDesc" | i18n }}
</span>
</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
*ngIf="!accessAllCollections"
[permissionMode]="PermissionMode.Edit"
formControlName="access"
[showGroupColumn]="organization.useGroups"
@@ -321,7 +217,6 @@
[columnHeader]="'collection' | i18n"
[selectorLabelText]="'selectCollections' | i18n"
[emptySelectionText]="'noCollectionsAdded' | i18n"
[flexibleCollectionsEnabled]="organization.flexibleCollections"
[hideMultiSelect]="restrictEditingSelf$ | async"
></bit-access-selector
></bit-tab>

View File

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

View File

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

View File

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

View File

@@ -110,15 +110,6 @@
</ng-container>
<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
*ngIf="item.readonly || disabled"
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
// 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();
}
}
@@ -196,21 +196,11 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
*/
@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
*/
@Input() hideMultiSelect = false;
private _flexibleCollectionsEnabled: boolean;
constructor(
private readonly formBuilder: FormBuilder,
private readonly i18nService: I18nService,
@@ -275,7 +265,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
}
async ngOnInit() {
this.permissionList = getPermissionList(this._flexibleCollectionsEnabled);
this.permissionList = getPermissionList();
// Watch the internal formArray for changes and propagate them
this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => {
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;
}
protected accessAllLabelId(item: AccessItemView) {
return item.type == AccessItemType.Group ? "groupAccessAll" : "memberAccessAll";
}
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) {

View File

@@ -34,12 +34,6 @@ export enum AccessItemType {
*
*/
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.
* This will disable the permission editor and will keep
@@ -82,16 +76,14 @@ export type Permission = {
labelId: string;
};
export const getPermissionList = (flexibleCollectionsEnabled: boolean): Permission[] => {
export const getPermissionList = (): Permission[] => {
const permissions = [
{ perm: CollectionPermission.View, labelId: "canView" },
{ perm: CollectionPermission.ViewExceptPass, labelId: "canViewExceptPass" },
{ perm: CollectionPermission.Edit, labelId: "canEdit" },
{ perm: CollectionPermission.EditExceptPass, labelId: "canEditExceptPass" },
{ perm: CollectionPermission.Manage, labelId: "canManage" },
];
if (flexibleCollectionsEnabled) {
permissions.push({ perm: CollectionPermission.Manage, labelId: "canManage" });
}
return permissions;
};
@@ -142,8 +134,6 @@ export function mapGroupToAccessItemView(group: GroupView): AccessItemView {
type: AccessItemType.Group,
listName: 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,
labelName: user.name ?? user.email,
status: user.status,
accessAllItems: user.accessAll,
readonly: user.accessAll,
};
}

View File

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

View File

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

View File

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

View File

@@ -176,12 +176,6 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { titleId: "updatePassword" } satisfies DataProperties,
},
{
path: "remove-password",
component: RemovePasswordComponent,
canActivate: [AuthGuard],
data: { titleId: "removeMasterPassword" } satisfies DataProperties,
},
{
path: "migrate-legacy-encryption",
loadComponent: () =>
@@ -195,25 +189,6 @@ const routes: Routes = [
path: "",
component: AnonLayoutWrapperComponent,
children: [
{
path: "recover-2fa",
canActivate: [unauthGuardFn()],
children: [
{
path: "",
component: RecoverTwoFactorComponent,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
data: {
pageTitle: "recoverAccountTwoStep",
titleId: "recoverAccountTwoStep",
} satisfies DataProperties & AnonLayoutWrapperData,
},
{
path: "accept-emergency",
canActivate: [deepLinkGuard()],
@@ -237,6 +212,34 @@ const routes: Routes = [
},
],
},
{
path: "recover-2fa",
canActivate: [unauthGuardFn()],
children: [
{
path: "",
component: RecoverTwoFactorComponent,
},
{
path: "",
component: EnvironmentSelectorComponent,
outlet: "environment-selector",
},
],
data: {
pageTitle: "recoverAccountTwoStep",
titleId: "recoverAccountTwoStep",
} satisfies DataProperties & AnonLayoutWrapperData,
},
{
path: "remove-password",
component: RemovePasswordComponent,
canActivate: [AuthGuard],
data: {
pageTitle: "removeMasterPassword",
titleId: "removeMasterPassword",
} satisfies DataProperties & AnonLayoutWrapperData,
},
],
},
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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