1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

Merge branch 'main' into ps/extension-refresh

This commit is contained in:
Victoria League
2024-08-14 10:53:36 -04:00
committed by GitHub
471 changed files with 18849 additions and 3310 deletions

View File

@@ -19,9 +19,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -96,9 +94,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
private organization$ = this.organizationService
.get$(this.organizationId)
.pipe(shareReplay({ refCount: true }));
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
);
protected PermissionMode = PermissionMode;
protected ResultType = GroupAddEditDialogResultType;
@@ -179,27 +174,19 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }),
);
protected allowAdminAccessToAllCollectionItems$ = combineLatest([
this.organization$,
this.flexibleCollectionsV1Enabled$,
]).pipe(
map(([organization, flexibleCollectionsV1Enabled]) => {
if (!flexibleCollectionsV1Enabled) {
return true;
}
protected allowAdminAccessToAllCollectionItems$ = this.organization$.pipe(
map((organization) => {
return organization.allowAdminAccessToAllCollectionItems;
}),
);
protected canAssignAccessToAnyCollection$ = combineLatest([
this.organization$,
this.flexibleCollectionsV1Enabled$,
this.allowAdminAccessToAllCollectionItems$,
]).pipe(
map(
([org, flexibleCollectionsV1Enabled, allowAdminAccessToAllCollectionItems]) =>
org.canEditAnyCollection(flexibleCollectionsV1Enabled) ||
([org, allowAdminAccessToAllCollectionItems]) =>
org.canEditAnyCollection ||
// Manage Groups custom permission cannot edit any collection but they can assign access from this dialog
// if permitted by collection management settings
(org.permissions.manageGroups && allowAdminAccessToAllCollectionItems),
@@ -224,7 +211,6 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
private changeDetectorRef: ChangeDetectorRef,
private dialogService: DialogService,
private organizationService: OrganizationService,
private configService: ConfigService,
private accountService: AccountService,
private collectionAdminService: CollectionAdminService,
) {
@@ -242,27 +228,13 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
this.cannotAddSelfToGroup$,
this.accountService.activeAccount$,
this.organization$,
this.flexibleCollectionsV1Enabled$,
])
.pipe(takeUntil(this.destroy$))
.subscribe(
([
collections,
members,
group,
restrictGroupAccess,
activeAccount,
organization,
flexibleCollectionsV1Enabled,
]) => {
([collections, members, group, restrictGroupAccess, activeAccount, organization]) => {
this.members = members;
this.group = group;
this.collections = mapToAccessItemViews(
collections,
organization,
flexibleCollectionsV1Enabled,
group,
);
this.collections = mapToAccessItemViews(collections, organization, group);
if (this.group != undefined) {
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
@@ -384,7 +356,6 @@ function mapToAccessSelections(group: GroupView, items: AccessItemView[]): Acces
function mapToAccessItemViews(
collections: CollectionAdminView[],
organization: Organization,
flexibleCollectionsV1Enabled: boolean,
group?: GroupView,
): AccessItemView[] {
return (
@@ -396,7 +367,7 @@ function mapToAccessItemViews(
type: AccessItemType.Collection,
labelName: c.name,
listName: c.name,
readonly: !c.canEditGroupAccess(organization, flexibleCollectionsV1Enabled),
readonly: !c.canEditGroupAccess(organization),
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
};
})

View File

@@ -23,8 +23,6 @@ import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permi
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
@@ -143,7 +141,6 @@ export class MemberDialogComponent implements OnDestroy {
private userService: UserAdminService,
private organizationUserService: OrganizationUserService,
private dialogService: DialogService,
private configService: ConfigService,
private accountService: AccountService,
organizationService: OrganizationService,
) {
@@ -174,15 +171,8 @@ export class MemberDialogComponent implements OnDestroy {
? this.userService.get(this.params.organizationId, this.params.organizationUserId)
: of(null);
this.allowAdminAccessToAllCollectionItems$ = combineLatest([
this.organization$,
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
]).pipe(
map(([organization, flexibleCollectionsV1Enabled]) => {
if (!flexibleCollectionsV1Enabled) {
return true;
}
this.allowAdminAccessToAllCollectionItems$ = this.organization$.pipe(
map((organization) => {
return organization.allowAdminAccessToAllCollectionItems;
}),
);
@@ -208,18 +198,13 @@ export class MemberDialogComponent implements OnDestroy {
}
});
const flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
);
this.canAssignAccessToAnyCollection$ = combineLatest([
this.organization$,
flexibleCollectionsV1Enabled$,
this.allowAdminAccessToAllCollectionItems$,
]).pipe(
map(
([org, flexibleCollectionsV1Enabled, allowAdminAccessToAllCollectionItems]) =>
org.canEditAnyCollection(flexibleCollectionsV1Enabled) ||
([org, allowAdminAccessToAllCollectionItems]) =>
org.canEditAnyCollection ||
// Manage Users custom permission cannot edit any collection but they can assign access from this dialog
// if permitted by collection management settings
(org.permissions.manageUsers && allowAdminAccessToAllCollectionItems),
@@ -231,49 +216,39 @@ export class MemberDialogComponent implements OnDestroy {
collections: this.collectionAdminService.getAll(this.params.organizationId),
userDetails: userDetails$,
groups: groups$,
flexibleCollectionsV1Enabled: flexibleCollectionsV1Enabled$,
})
.pipe(takeUntil(this.destroy$))
.subscribe(
({ organization, collections, userDetails, groups, flexibleCollectionsV1Enabled }) => {
this.setFormValidators(organization);
.subscribe(({ organization, collections, userDetails, groups }) => {
this.setFormValidators(organization);
// Groups tab: populate available groups
this.groupAccessItems = [].concat(
groups.map<AccessItemView>((g) => mapGroupToAccessItemView(g)),
// Groups tab: populate available groups
this.groupAccessItems = [].concat(
groups.map<AccessItemView>((g) => mapGroupToAccessItemView(g)),
);
// Collections tab: Populate all available collections (including current user access where applicable)
this.collectionAccessItems = collections
.map((c) =>
mapCollectionToAccessItemView(
c,
organization,
userDetails == null
? undefined
: c.users.find((access) => access.id === userDetails.id),
),
)
// But remove collections that we can't assign access to, unless the user is already assigned
.filter(
(item) =>
!item.readonly || userDetails?.collections.some((access) => access.id == item.id),
);
// Collections tab: Populate all available collections (including current user access where applicable)
this.collectionAccessItems = collections
.map((c) =>
mapCollectionToAccessItemView(
c,
organization,
flexibleCollectionsV1Enabled,
userDetails == null
? undefined
: c.users.find((access) => access.id === userDetails.id),
),
)
// But remove collections that we can't assign access to, unless the user is already assigned
.filter(
(item) =>
!item.readonly || userDetails?.collections.some((access) => access.id == item.id),
);
if (userDetails != null) {
this.loadOrganizationUser(userDetails, groups, collections, organization);
}
if (userDetails != null) {
this.loadOrganizationUser(
userDetails,
groups,
collections,
organization,
flexibleCollectionsV1Enabled,
);
}
this.loading = false;
},
);
this.loading = false;
});
}
private setFormValidators(organization: Organization) {
@@ -297,7 +272,6 @@ export class MemberDialogComponent implements OnDestroy {
groups: GroupView[],
collections: CollectionAdminView[],
organization: Organization,
flexibleCollectionsV1Enabled: boolean,
) {
if (!userDetails) {
throw new Error("Could not find user to edit.");
@@ -341,13 +315,7 @@ export class MemberDialogComponent implements OnDestroy {
// Populate additional collection access via groups (rendered as separate rows from user access)
this.collectionAccessItems = this.collectionAccessItems.concat(
collectionsFromGroups.map(({ collection, accessSelection, group }) =>
mapCollectionToAccessItemView(
collection,
organization,
flexibleCollectionsV1Enabled,
accessSelection,
group,
),
mapCollectionToAccessItemView(collection, organization, accessSelection, group),
),
);
@@ -621,7 +589,6 @@ export class MemberDialogComponent implements OnDestroy {
function mapCollectionToAccessItemView(
collection: CollectionAdminView,
organization: Organization,
flexibleCollectionsV1Enabled: boolean,
accessSelection?: CollectionAccessSelectionView,
group?: GroupView,
): AccessItemView {
@@ -630,9 +597,7 @@ function mapCollectionToAccessItemView(
id: group ? `${collection.id}-${group.id}` : collection.id,
labelName: collection.name,
listName: collection.name,
readonly:
group !== undefined ||
!collection.canEditUserAccess(organization, flexibleCollectionsV1Enabled),
readonly: group !== undefined || !collection.canEditUserAccess(organization),
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
viaGroupName: group?.name,
};

View File

@@ -56,7 +56,7 @@
>
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "collectionManagement" | i18n }}</h1>
<p bitTypography="body1">{{ "collectionManagementDesc" | i18n }}</p>
<bit-form-control *ngIf="flexibleCollectionsV1Enabled$ | async">
<bit-form-control>
<bit-label>{{ "allowAdminAccessToAllCollectionItemsDesc" | i18n }}</bit-label>
<input type="checkbox" bitCheckbox formControlName="allowAdminAccessToAllCollectionItems" />
</bit-form-control>

View File

@@ -10,8 +10,6 @@ import { OrganizationCollectionManagementUpdateRequest } from "@bitwarden/common
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-update.request";
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -40,10 +38,6 @@ export class AccountComponent implements OnInit, OnDestroy {
org: OrganizationResponse;
taxFormPromise: Promise<unknown>;
flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
);
// FormGroup validators taken from server Organization domain object
protected formGroup = this.formBuilder.group({
orgName: this.formBuilder.control(
@@ -83,7 +77,6 @@ export class AccountComponent implements OnInit, OnDestroy {
private organizationApiService: OrganizationApiServiceAbstraction,
private dialogService: DialogService,
private formBuilder: FormBuilder,
private configService: ConfigService,
) {}
async ngOnInit() {

View File

@@ -94,7 +94,7 @@ export class AppComponent implements OnDestroy, OnInit {
private policyService: InternalPolicyService,
protected policyListService: PolicyListService,
private keyConnectorService: KeyConnectorService,
private configService: ConfigService,
protected configService: ConfigService,
private dialogService: DialogService,
private biometricStateService: BiometricStateService,
private stateEventRunnerService: StateEventRunnerService,

View File

@@ -25,13 +25,7 @@
</bit-form-field>
<bit-form-field>
<bit-label>{{ "twoFactorDuoClientSecret" | i18n }}</bit-label>
<input
bitInput
type="password"
formControlName="clientSecret"
appInputVerbatim
autocomplete="new-password"
/>
<input bitInput type="password" formControlName="clientSecret" appInputVerbatim />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "twoFactorDuoApiHostname" | i18n }}</bit-label>

View File

@@ -37,14 +37,7 @@
</picture>
<bit-form-field>
<bit-label class="tw-sr-only">{{ "verificationCode" | i18n }}</bit-label>
<input
type="password"
bitInput
formControlName="token"
appAutofocus
appInputVerbatim
autocomplete="new-password"
/>
<input type="password" bitInput formControlName="token" appAutofocus appInputVerbatim />
</bit-form-field>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">

View File

@@ -65,9 +65,11 @@ export class PremiumComponent implements OnInit {
}
}
submit = async () => {
if (!this.taxInfoComponent?.taxFormGroup.valid && this.taxInfoComponent?.taxFormGroup.touched) {
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
return;
if (this.taxInfoComponent) {
if (!this.taxInfoComponent?.taxFormGroup.valid) {
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
return;
}
}
this.licenseForm.markAllAsTouched();
this.addonForm.markAllAsTouched();

View File

@@ -5,8 +5,9 @@
<bit-label>{{ "subscriptionSeats" | i18n }}</bit-label>
<input bitInput formControlName="newSeatCount" type="number" min="0" step="1" />
<bit-hint>
<strong>{{ "total" | i18n }}:</strong> {{ additionalSeatCount || 0 }} &times;
{{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} /
<strong>{{ "total" | i18n }}:</strong>
{{ adjustSubscriptionForm.value.newSeatCount || 0 }} &times;
{{ seatPrice | currency: "$" }} = {{ seatTotalCost | currency: "$" }} /
{{ interval | i18n }}</bit-hint
>
</bit-form-field>
@@ -43,7 +44,8 @@
step="1"
/>
<bit-hint>
<strong>{{ "maxSeatCost" | i18n }}:</strong> {{ additionalMaxSeatCount || 0 }} &times;
<strong>{{ "maxSeatCost" | i18n }}:</strong>
{{ adjustSubscriptionForm.value.newMaxSeats || 0 }} &times;
{{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} /
{{ interval | i18n }}</bit-hint
>

View File

@@ -99,11 +99,12 @@ export class AdjustSubscription implements OnInit, OnDestroy {
: 0;
}
get adjustedSeatTotal(): number {
return this.additionalSeatCount * this.seatPrice;
get maxSeatTotal(): number {
return Math.abs((this.adjustSubscriptionForm.value.newMaxSeats ?? 0) * this.seatPrice);
}
get maxSeatTotal(): number {
return this.additionalMaxSeatCount * this.seatPrice;
get seatTotalCost(): number {
const totalSeat = Math.abs(this.adjustSubscriptionForm.value.newSeatCount * this.seatPrice);
return totalSeat;
}
}

View File

@@ -554,9 +554,11 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
}
submit = async () => {
if (!this.taxComponent?.taxFormGroup.valid && this.taxComponent?.taxFormGroup.touched) {
this.taxComponent?.taxFormGroup.markAllAsTouched();
return;
if (this.taxComponent) {
if (!this.taxComponent?.taxFormGroup.valid) {
this.taxComponent?.taxFormGroup.markAllAsTouched();
return;
}
}
if (this.singleOrgPolicyBlock) {

View File

@@ -49,6 +49,7 @@ import { StorageServiceProvider } from "@bitwarden/common/platform/services/stor
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
import {
DefaultThemeStateService,
ThemeStateService,
@@ -63,7 +64,6 @@ import { I18nService } from "../core/i18n.service";
import { WebEnvironmentService } from "../platform/web-environment.service";
import { WebMigrationRunner } from "../platform/web-migration-runner";
import { WebStorageServiceProvider } from "../platform/web-storage-service.provider";
import { WindowStorageService } from "../platform/window-storage.service";
import { CollectionAdminService } from "../vault/core/collection-admin.service";
import { EventService } from "./event.service";

View File

@@ -3,11 +3,11 @@ import { MockProxy, mock } from "jest-mock-extended";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
import { MigrationBuilder } from "@bitwarden/common/state-migrations/migration-builder";
import { MigrationHelper } from "@bitwarden/common/state-migrations/migration-helper";
import { WebMigrationRunner } from "./web-migration-runner";
import { WindowStorageService } from "./window-storage.service";
describe("WebMigrationRunner", () => {
let logService: MockProxy<LogService>;

View File

@@ -4,10 +4,9 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
import { MigrationHelper } from "@bitwarden/common/state-migrations/migration-helper";
import { WindowStorageService } from "./window-storage.service";
export class WebMigrationRunner extends MigrationRunner {
constructor(
diskStorage: AbstractStorageService,

View File

@@ -1,57 +0,0 @@
import { Observable, Subject } from "rxjs";
import {
AbstractStorageService,
ObservableStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
export class WindowStorageService implements AbstractStorageService, ObservableStorageService {
private readonly updatesSubject = new Subject<StorageUpdate>();
updates$: Observable<StorageUpdate>;
constructor(private readonly storage: Storage) {
this.updates$ = this.updatesSubject.asObservable();
}
get valuesRequireDeserialization(): boolean {
return true;
}
get<T>(key: string, options?: StorageOptions): Promise<T> {
const jsonValue = this.storage.getItem(key);
if (jsonValue != null) {
return Promise.resolve(JSON.parse(jsonValue) as T);
}
return Promise.resolve(null);
}
async has(key: string, options?: StorageOptions): Promise<boolean> {
return (await this.get(key, options)) != null;
}
save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
if (obj == null) {
return this.remove(key, options);
}
if (obj instanceof Set) {
obj = Array.from(obj) as T;
}
this.storage.setItem(key, JSON.stringify(obj));
this.updatesSubject.next({ key, updateType: "save" });
}
remove(key: string, options?: StorageOptions): Promise<void> {
this.storage.removeItem(key);
this.updatesSubject.next({ key, updateType: "remove" });
return Promise.resolve();
}
getKeys(): string[] {
return Object.keys(this.storage);
}
}

View File

@@ -80,13 +80,9 @@
<span *ngIf="!organization.useGroups">{{
"grantCollectionAccessMembersOnly" | i18n
}}</span>
<span
*ngIf="
(flexibleCollectionsV1Enabled$ | async) &&
organization.allowAdminAccessToAllCollectionItems
"
>{{ " " + ("adminCollectionAccess" | i18n) }}</span
>
<span *ngIf="organization.allowAdminAccessToAllCollectionItems">
{{ " " + ("adminCollectionAccess" | i18n) }}
</span>
</ng-container>
</div>
<div

View File

@@ -17,8 +17,6 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses/organization-user.response";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -77,10 +75,6 @@ export enum CollectionDialogAction {
templateUrl: "collection-dialog.component.html",
})
export class CollectionDialogComponent implements OnInit, OnDestroy {
protected flexibleCollectionsV1Enabled$ = this.configService
.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1)
.pipe(first());
private destroy$ = new Subject<void>();
protected organizations$: Observable<Organization[]>;
@@ -113,7 +107,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private organizationUserService: OrganizationUserService,
private configService: ConfigService,
private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef,
) {
@@ -163,95 +156,90 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
groups: groups$,
// Collection(s) needed to map readonlypermission for (potential) access selector disabled state
users: this.organizationUserService.getAllUsers(orgId, { includeCollections: true }),
flexibleCollectionsV1: this.flexibleCollectionsV1Enabled$,
})
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
.subscribe(
({ organization, collections: allCollections, groups, users, flexibleCollectionsV1 }) => {
this.organization = organization;
this.accessItems = [].concat(
groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)),
users.data.map((user) => mapUserToAccessItemView(user, this.collectionId)),
);
.subscribe(({ organization, collections: allCollections, groups, users }) => {
this.organization = organization;
this.accessItems = [].concat(
groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)),
users.data.map((user) => mapUserToAccessItemView(user, this.collectionId)),
);
// Force change detection to update the access selector's items
this.changeDetectorRef.detectChanges();
// Force change detection to update the access selector's items
this.changeDetectorRef.detectChanges();
this.nestOptions = this.params.limitNestedCollections
? allCollections.filter((c) => c.manage)
: allCollections;
this.nestOptions = this.params.limitNestedCollections
? allCollections.filter((c) => c.manage)
: allCollections;
if (this.params.collectionId) {
this.collection = allCollections.find((c) => c.id === this.collectionId);
// Ensure we don't allow nesting the current collection within itself
this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId);
if (this.params.collectionId) {
this.collection = allCollections.find((c) => c.id === this.collectionId);
// Ensure we don't allow nesting the current collection within itself
this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId);
if (!this.collection) {
throw new Error("Could not find collection to edit.");
}
// Parse the name to find its parent name
const { name, parent: parentName } = parseName(this.collection);
// Determine if the user can see/select the parent collection
if (parentName !== undefined) {
if (
this.organization.canViewAllCollections &&
!allCollections.find((c) => c.name === parentName)
) {
// The user can view all collections, but the parent was not found -> assume it has been deleted
this.deletedParentName = parentName;
} else if (!this.nestOptions.find((c) => c.name === parentName)) {
// We cannot find the current parent collection in our list of options, so add a placeholder
this.nestOptions.unshift({ name: parentName } as CollectionView);
}
}
const accessSelections = mapToAccessSelections(this.collection);
this.formGroup.patchValue({
name,
externalId: this.collection.externalId,
parent: parentName,
access: accessSelections,
});
this.showDeleteButton =
!this.dialogReadonly &&
this.collection.canDelete(organization, flexibleCollectionsV1);
} else {
const parent = this.nestOptions.find((c) => c.id === this.params.parentCollectionId);
const currentOrgUserId = users.data.find(
(u) => u.userId === this.organization?.userId,
)?.id;
const initialSelection: AccessItemValue[] =
currentOrgUserId !== undefined
? [
{
id: currentOrgUserId,
type: AccessItemType.Member,
permission: CollectionPermission.Manage,
},
]
: [];
this.formGroup.patchValue({
parent: parent?.name ?? undefined,
access: initialSelection,
});
if (!this.collection) {
throw new Error("Could not find collection to edit.");
}
if (flexibleCollectionsV1 && !organization.allowAdminAccessToAllCollectionItems) {
this.formGroup.controls.access.addValidators(validateCanManagePermission);
} else {
this.formGroup.controls.access.removeValidators(validateCanManagePermission);
// Parse the name to find its parent name
const { name, parent: parentName } = parseName(this.collection);
// Determine if the user can see/select the parent collection
if (parentName !== undefined) {
if (
this.organization.canViewAllCollections &&
!allCollections.find((c) => c.name === parentName)
) {
// The user can view all collections, but the parent was not found -> assume it has been deleted
this.deletedParentName = parentName;
} else if (!this.nestOptions.find((c) => c.name === parentName)) {
// We cannot find the current parent collection in our list of options, so add a placeholder
this.nestOptions.unshift({ name: parentName } as CollectionView);
}
}
this.formGroup.controls.access.updateValueAndValidity();
this.handleFormGroupReadonly(this.dialogReadonly);
const accessSelections = mapToAccessSelections(this.collection);
this.formGroup.patchValue({
name,
externalId: this.collection.externalId,
parent: parentName,
access: accessSelections,
});
this.showDeleteButton = !this.dialogReadonly && this.collection.canDelete(organization);
} else {
const parent = this.nestOptions.find((c) => c.id === this.params.parentCollectionId);
const currentOrgUserId = users.data.find(
(u) => u.userId === this.organization?.userId,
)?.id;
const initialSelection: AccessItemValue[] =
currentOrgUserId !== undefined
? [
{
id: currentOrgUserId,
type: AccessItemType.Member,
permission: CollectionPermission.Manage,
},
]
: [];
this.loading = false;
this.showAddAccessWarning = this.handleAddAccessWarning(flexibleCollectionsV1);
},
);
this.formGroup.patchValue({
parent: parent?.name ?? undefined,
access: initialSelection,
});
}
if (!organization.allowAdminAccessToAllCollectionItems) {
this.formGroup.controls.access.addValidators(validateCanManagePermission);
} else {
this.formGroup.controls.access.removeValidators(validateCanManagePermission);
}
this.formGroup.controls.access.updateValueAndValidity();
this.handleFormGroupReadonly(this.dialogReadonly);
this.loading = false;
this.showAddAccessWarning = this.handleAddAccessWarning();
});
}
protected get collectionId() {
@@ -361,9 +349,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
this.destroy$.complete();
}
private handleAddAccessWarning(flexibleCollectionsV1: boolean): boolean {
private handleAddAccessWarning(): boolean {
if (
flexibleCollectionsV1 &&
!this.organization?.allowAdminAccessToAllCollectionItems &&
this.params.isAddAccessCollection
) {

View File

@@ -34,7 +34,6 @@ export class VaultCollectionRowComponent {
@Input() organizations: Organization[];
@Input() groups: GroupView[];
@Input() showPermissionsColumn: boolean;
@Input() flexibleCollectionsV1Enabled: boolean;
@Input() restrictProviderAccess: boolean;
@Output() onEvent = new EventEmitter<VaultItemEvent>();
@@ -57,10 +56,6 @@ export class VaultCollectionRowComponent {
}
get showAddAccess() {
if (!this.flexibleCollectionsV1Enabled) {
return false;
}
if (this.collection.id == Unassigned) {
return false;
}
@@ -71,7 +66,7 @@ export class VaultCollectionRowComponent {
return (
!this.organization?.allowAdminAccessToAllCollectionItems &&
this.collection.unmanaged &&
this.organization?.canEditUnmanagedCollections()
this.organization?.canEditUnmanagedCollections
);
}
@@ -114,10 +109,6 @@ export class VaultCollectionRowComponent {
}
protected get showCheckbox() {
if (this.flexibleCollectionsV1Enabled) {
return this.collection?.id !== Unassigned;
}
return this.canDeleteCollection;
return this.collection?.id !== Unassigned;
}
}

View File

@@ -113,7 +113,6 @@
[canDeleteCollection]="canDeleteCollection(item.collection)"
[canEditCollection]="canEditCollection(item.collection)"
[canViewCollectionInfo]="canViewCollectionInfo(item.collection)"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
[restrictProviderAccess]="restrictProviderAccess"
[checked]="selection.isSelected(item)"
(checkedToggled)="selection.toggle(item)"

View File

@@ -44,7 +44,6 @@ export class VaultItemsComponent {
@Input() showBulkAddToCollections = false;
@Input() showPermissionsColumn = false;
@Input() viewingOrgVault: boolean;
@Input({ required: true }) flexibleCollectionsV1Enabled = false;
@Input() addAccessStatus: number;
@Input() addAccessToggle: boolean;
@Input() restrictProviderAccess: boolean;
@@ -120,7 +119,7 @@ export class VaultItemsComponent {
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
return collection.canEdit(organization, this.flexibleCollectionsV1Enabled);
return collection.canEdit(organization);
}
protected canDeleteCollection(collection: CollectionView): boolean {
@@ -131,12 +130,12 @@ export class VaultItemsComponent {
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
return collection.canDelete(organization, this.flexibleCollectionsV1Enabled);
return collection.canDelete(organization);
}
protected canViewCollectionInfo(collection: CollectionView) {
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
return collection.canViewCollectionInfo(organization, this.flexibleCollectionsV1Enabled);
return collection.canViewCollectionInfo(organization);
}
protected toggleAll() {
@@ -214,11 +213,7 @@ export class VaultItemsComponent {
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
return (
(organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
) &&
this.viewingOrgVault) ||
(organization.canEditAllCiphers(this.restrictProviderAccess) && this.viewingOrgVault) ||
cipher.edit
);
}
@@ -230,21 +225,12 @@ export class VaultItemsComponent {
this.selection.clear();
if (this.flexibleCollectionsV1Enabled) {
// Every item except for the Unassigned collection is selectable, individual bulk actions check the user's permission
this.editableItems = items.filter(
(item) =>
item.cipher !== undefined ||
(item.collection !== undefined && item.collection.id !== Unassigned),
);
} else {
// only collections the user can delete are selectable
this.editableItems = items.filter(
(item) =>
item.cipher !== undefined ||
(item.collection !== undefined && this.canDeleteCollection(item.collection)),
);
}
// Every item except for the Unassigned collection is selectable, individual bulk actions check the user's permission
this.editableItems = items.filter(
(item) =>
item.cipher !== undefined ||
(item.collection !== undefined && item.collection.id !== Unassigned),
);
this.dataSource.data = items;
}
@@ -293,10 +279,7 @@ export class VaultItemsComponent {
const organization = this.allOrganizations.find((o) => o.id === orgId);
const canEditOrManageAllCiphers =
organization?.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
) && this.viewingOrgVault;
organization?.canEditAllCiphers(this.restrictProviderAccess) && this.viewingOrgVault;
const collectionNotSelected =
this.selection.selected.filter((item) => item.collection).length === 0;
@@ -317,9 +300,7 @@ export class VaultItemsComponent {
const canEditOrManageAllCiphers =
organizations.length > 0 &&
organizations.every((org) =>
org?.canEditAllCiphers(this.flexibleCollectionsV1Enabled, this.restrictProviderAccess),
);
organizations.every((org) => org?.canEditAllCiphers(this.restrictProviderAccess));
const canDeleteCollections = this.selection.selected
.filter((item) => item.collection)

View File

@@ -41,61 +41,44 @@ export class CollectionAdminView extends CollectionView {
/**
* Returns true if the user can edit a collection (including user and group access) from the Admin Console.
*/
override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
override canEdit(org: Organization): boolean {
return (
org?.canEditAnyCollection(flexibleCollectionsV1Enabled) ||
(flexibleCollectionsV1Enabled && this.unmanaged && org?.canEditUnmanagedCollections()) ||
super.canEdit(org, flexibleCollectionsV1Enabled)
org?.canEditAnyCollection ||
(this.unmanaged && org?.canEditUnmanagedCollections) ||
super.canEdit(org)
);
}
/**
* Returns true if the user can delete a collection from the Admin Console.
*/
override canDelete(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
return (
org?.canDeleteAnyCollection(flexibleCollectionsV1Enabled) ||
super.canDelete(org, flexibleCollectionsV1Enabled)
);
override canDelete(org: Organization): boolean {
return org?.canDeleteAnyCollection || super.canDelete(org);
}
/**
* Whether the user can modify user access to this collection
*/
canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
const allowAdminAccessToAllCollectionItems =
!flexibleCollectionsV1Enabled || org.allowAdminAccessToAllCollectionItems;
canEditUserAccess(org: Organization): boolean {
return (
(org.permissions.manageUsers && allowAdminAccessToAllCollectionItems) ||
this.canEdit(org, flexibleCollectionsV1Enabled)
(org.permissions.manageUsers && org.allowAdminAccessToAllCollectionItems) || this.canEdit(org)
);
}
/**
* Whether the user can modify group access to this collection
*/
canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean {
const allowAdminAccessToAllCollectionItems =
!flexibleCollectionsV1Enabled || org.allowAdminAccessToAllCollectionItems;
canEditGroupAccess(org: Organization): boolean {
return (
(org.permissions.manageGroups && allowAdminAccessToAllCollectionItems) ||
this.canEdit(org, flexibleCollectionsV1Enabled)
(org.permissions.manageGroups && org.allowAdminAccessToAllCollectionItems) ||
this.canEdit(org)
);
}
/**
* Returns true if the user can view collection info and access in a read-only state from the Admin Console
*/
override canViewCollectionInfo(
org: Organization | undefined,
flexibleCollectionsV1Enabled: boolean,
): boolean {
if (!flexibleCollectionsV1Enabled) {
return false;
}
override canViewCollectionInfo(org: Organization | undefined): boolean {
if (this.isUnassignedCollection) {
return false;
}

View File

@@ -54,10 +54,6 @@ export class BulkDeleteDialogComponent {
collections: CollectionView[];
unassignedCiphers: string[];
private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
);
private restrictProviderAccess$ = this.configService.getFeatureFlag$(
FeatureFlag.RestrictProviderAccess,
);
@@ -96,13 +92,9 @@ export class BulkDeleteDialogComponent {
deletePromises.push(this.deleteCiphersAdmin(this.unassignedCiphers));
}
if (this.cipherIds.length) {
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
if (
!this.organization ||
!this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled, restrictProviderAccess)
) {
if (!this.organization || !this.organization.canEditAllCiphers(restrictProviderAccess)) {
deletePromises.push(this.deleteCiphers());
} else {
deletePromises.push(this.deleteCiphersAdmin(this.cipherIds));
@@ -134,12 +126,8 @@ export class BulkDeleteDialogComponent {
};
private async deleteCiphers(): Promise<any> {
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$);
const asAdmin = this.organization?.canEditAllCiphers(
flexibleCollectionsV1Enabled,
restrictProviderAccess,
);
const asAdmin = this.organization?.canEditAllCiphers(restrictProviderAccess);
if (this.permanent) {
await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin);
} else {
@@ -157,12 +145,9 @@ export class BulkDeleteDialogComponent {
}
private async deleteCollections(): Promise<any> {
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
// From org vault
if (this.organization) {
if (
this.collections.some((c) => !c.canDelete(this.organization, flexibleCollectionsV1Enabled))
) {
if (this.collections.some((c) => !c.canDelete(this.organization))) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
@@ -179,7 +164,7 @@ export class BulkDeleteDialogComponent {
const deletePromises: Promise<any>[] = [];
for (const organization of this.organizations) {
const orgCollections = this.collections.filter((o) => o.organizationId === organization.id);
if (orgCollections.some((c) => !c.canDelete(organization, flexibleCollectionsV1Enabled))) {
if (orgCollections.some((c) => !c.canDelete(organization))) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),

View File

@@ -32,13 +32,7 @@
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
appStopProp
[disabled]="
!c.canEditItems(
this.organization,
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess
)
"
[disabled]="!c.canEditItems(this.organization, this.restrictProviderAccess)"
/>
{{ c.name }}
</td>

View File

@@ -50,13 +50,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
}
check(c: CollectionView, select?: boolean) {
if (
!c.canEditItems(
this.organization,
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
)
) {
if (!c.canEditItems(this.organization, this.restrictProviderAccess)) {
return;
}
(c as any).checked = select == null ? !(c as any).checked : select;

View File

@@ -36,6 +36,8 @@ describe("vault filter service", () => {
let organizations: ReplaySubject<Organization[]>;
let folderViews: ReplaySubject<FolderView[]>;
let collectionViews: ReplaySubject<CollectionView[]>;
let personalOwnershipPolicy: ReplaySubject<boolean>;
let singleOrgPolicy: ReplaySubject<boolean>;
let stateProvider: FakeStateProvider;
const mockUserId = Utils.newGuid() as UserId;
@@ -56,10 +58,18 @@ describe("vault filter service", () => {
organizations = new ReplaySubject<Organization[]>(1);
folderViews = new ReplaySubject<FolderView[]>(1);
collectionViews = new ReplaySubject<CollectionView[]>(1);
personalOwnershipPolicy = new ReplaySubject<boolean>(1);
singleOrgPolicy = new ReplaySubject<boolean>(1);
organizationService.memberOrganizations$ = organizations;
folderService.folderViews$ = folderViews;
collectionService.decryptedCollections$ = collectionViews;
policyService.policyAppliesToActiveUser$
.calledWith(PolicyType.PersonalOwnership)
.mockReturnValue(personalOwnershipPolicy);
policyService.policyAppliesToActiveUser$
.calledWith(PolicyType.SingleOrg)
.mockReturnValue(singleOrgPolicy);
vaultFilterService = new VaultFilterService(
organizationService,
@@ -100,6 +110,8 @@ describe("vault filter service", () => {
beforeEach(() => {
const storedOrgs = [createOrganization("1", "org1"), createOrganization("2", "org2")];
organizations.next(storedOrgs);
personalOwnershipPolicy.next(false);
singleOrgPolicy.next(false);
});
it("returns a nested tree", async () => {
@@ -111,9 +123,7 @@ describe("vault filter service", () => {
});
it("hides My Vault if personal ownership policy is enabled", async () => {
policyService.policyAppliesToUser
.calledWith(PolicyType.PersonalOwnership)
.mockResolvedValue(true);
personalOwnershipPolicy.next(true);
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
@@ -122,7 +132,7 @@ describe("vault filter service", () => {
});
it("returns 1 organization and My Vault if single organization policy is enabled", async () => {
policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true);
singleOrgPolicy.next(true);
const tree = await firstValueFrom(vaultFilterService.organizationTree$);
@@ -132,10 +142,8 @@ describe("vault filter service", () => {
});
it("returns 1 organization if both single organization and personal ownership policies are enabled", async () => {
policyService.policyAppliesToUser.calledWith(PolicyType.SingleOrg).mockResolvedValue(true);
policyService.policyAppliesToUser
.calledWith(PolicyType.PersonalOwnership)
.mockResolvedValue(true);
singleOrgPolicy.next(true);
personalOwnershipPolicy.next(true);
const tree = await firstValueFrom(vaultFilterService.organizationTree$);

View File

@@ -1,6 +1,7 @@
import { Injectable } from "@angular/core";
import {
BehaviorSubject,
combineLatest,
combineLatestWith,
firstValueFrom,
map,
@@ -39,10 +40,15 @@ const NestingDelimiter = "/";
@Injectable()
export class VaultFilterService implements VaultFilterServiceAbstraction {
organizationTree$: Observable<TreeNode<OrganizationFilter>> =
this.organizationService.memberOrganizations$.pipe(
switchMap((orgs) => this.buildOrganizationTree(orgs)),
);
organizationTree$: Observable<TreeNode<OrganizationFilter>> = combineLatest([
this.organizationService.memberOrganizations$,
this.policyService.policyAppliesToActiveUser$(PolicyType.SingleOrg),
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership),
]).pipe(
switchMap(([orgs, singleOrgPolicy, personalOwnershipPolicy]) =>
this.buildOrganizationTree(orgs, singleOrgPolicy, personalOwnershipPolicy),
),
);
protected _organizationFilter = new BehaviorSubject<Organization>(null);
@@ -125,14 +131,16 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
}
protected async buildOrganizationTree(
orgs?: Organization[],
orgs: Organization[],
singleOrgPolicy: boolean,
personalOwnershipPolicy: boolean,
): Promise<TreeNode<OrganizationFilter>> {
const headNode = this.getOrganizationFilterHead();
if (!(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership))) {
if (!personalOwnershipPolicy) {
const myVaultNode = this.getOrganizationFilterMyVault();
headNode.children.push(myVaultNode);
}
if (await this.policyService.policyAppliesToUser(PolicyType.SingleOrg)) {
if (singleOrgPolicy) {
orgs = orgs.slice(0, 1);
}
if (orgs) {

View File

@@ -69,30 +69,84 @@
<div *ngIf="filter.type !== 'trash'" class="tw-shrink-0">
<div appListDropdown>
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
{{ "new" | i18n }}<i class="bwi bwi-angle-down tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher()">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
{{ "item" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button *ngIf="canCreateCollections" type="button" bitMenuItem (click)="addCollection()">
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
<ng-container [ngSwitch]="extensionRefreshEnabled">
<ng-container *ngSwitchCase="true">
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
<bit-menu-divider />
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button
*ngIf="canCreateCollections"
type="button"
bitMenuItem
(click)="addCollection()"
>
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</ng-container>
<ng-container *ngSwitchCase="false">
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
{{ "new" | i18n }}<i class="bwi bwi-angle-down tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher()">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
{{ "item" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button
*ngIf="canCreateCollections"
type="button"
bitMenuItem
(click)="addCollection()"
>
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</ng-container>
</ng-container>
</div>
</div>
</app-header>

View File

@@ -14,6 +14,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { BreadcrumbsModule, MenuModule } from "@bitwarden/components";
@@ -47,6 +48,8 @@ export class VaultHeaderComponent implements OnInit {
protected Unassigned = Unassigned;
protected All = All;
protected CollectionDialogTabType = CollectionDialogTabType;
protected CipherType = CipherType;
protected extensionRefreshEnabled = false;
/**
* Boolean to determine the loading state of the header.
@@ -67,7 +70,7 @@ export class VaultHeaderComponent implements OnInit {
@Input() canCreateCollections: boolean;
/** Emits an event when the new item button is clicked in the header */
@Output() onAddCipher = new EventEmitter<void>();
@Output() onAddCipher = new EventEmitter<CipherType | undefined>();
/** Emits an event when the new collection button is clicked in the 'New' dropdown menu */
@Output() onAddCollection = new EventEmitter<null>();
@@ -81,16 +84,14 @@ export class VaultHeaderComponent implements OnInit {
/** Emits an event when the delete collection button is clicked in the header */
@Output() onDeleteCollection = new EventEmitter<void>();
private flexibleCollectionsV1Enabled = false;
constructor(
private i18nService: I18nService,
private configService: ConfigService,
) {}
async ngOnInit() {
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
this.extensionRefreshEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
}
@@ -174,7 +175,7 @@ export class VaultHeaderComponent implements OnInit {
const organization = this.organizations.find(
(o) => o.id === this.collection?.node.organizationId,
);
return this.collection.node.canEdit(organization, this.flexibleCollectionsV1Enabled);
return this.collection.node.canEdit(organization);
}
async editCollection(tab: CollectionDialogTabType): Promise<void> {
@@ -192,15 +193,15 @@ export class VaultHeaderComponent implements OnInit {
(o) => o.id === this.collection?.node.organizationId,
);
return this.collection.node.canDelete(organization, this.flexibleCollectionsV1Enabled);
return this.collection.node.canDelete(organization);
}
deleteCollection() {
this.onDeleteCollection.emit();
}
protected addCipher() {
this.onAddCipher.emit();
protected addCipher(cipherType?: CipherType) {
this.onAddCipher.emit(cipherType);
}
async addFolder(): Promise<void> {

View File

@@ -22,7 +22,12 @@
<p class="tw-pl-1">
{{ "onboardingImportDataDetailsPartOne" | i18n }}
<button type="button" bitLink (click)="emitToAddCipher()">
{{ "onboardingImportDataDetailsLink" | i18n }}
{{
(extensionRefreshEnabled
? "onboardingImportDataDetailsLoginLink"
: "onboardingImportDataDetailsLink"
) | i18n
}}
</button>
<span>
{{ "onboardingImportDataDetailsPartTwoNoOrgs" | i18n }}

View File

@@ -5,9 +5,11 @@ import { Subject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service";
@@ -24,6 +26,7 @@ describe("VaultOnboardingComponent", () => {
let mockStateProvider: Partial<StateProvider>;
let setInstallExtLinkSpy: any;
let individualVaultPolicyCheckSpy: any;
let mockConfigService: MockProxy<ConfigService>;
beforeEach(() => {
mockPolicyService = mock<PolicyService>();
@@ -42,6 +45,7 @@ describe("VaultOnboardingComponent", () => {
}),
),
};
mockConfigService = mock<ConfigService>();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({
@@ -54,6 +58,7 @@ describe("VaultOnboardingComponent", () => {
{ provide: I18nService, useValue: mockI18nService },
{ provide: ApiService, useValue: mockApiService },
{ provide: StateProvider, useValue: mockStateProvider },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compileComponents();
fixture = TestBed.createComponent(VaultOnboardingComponent);
@@ -178,4 +183,14 @@ describe("VaultOnboardingComponent", () => {
expect(saveCompletedTasksSpy).toHaveBeenCalled();
});
});
describe("emitToAddCipher", () => {
it("always emits the `CipherType.Login` type when called", () => {
const emitSpy = jest.spyOn(component.onAddCipher, "emit");
component.emitToAddCipher();
expect(emitSpy).toHaveBeenCalledWith(CipherType.Login);
});
});
});

View File

@@ -16,7 +16,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LinkModule } from "@bitwarden/components";
@@ -41,7 +44,7 @@ import { VaultOnboardingService, VaultOnboardingTasks } from "./services/vault-o
export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
@Input() ciphers: CipherView[];
@Input() orgs: Organization[];
@Output() onAddCipher = new EventEmitter<void>();
@Output() onAddCipher = new EventEmitter<CipherType>();
extensionUrl: string;
isIndividualPolicyVault: boolean;
@@ -53,12 +56,14 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
protected onboardingTasks$: Observable<VaultOnboardingTasks>;
protected showOnboarding = false;
protected extensionRefreshEnabled = false;
constructor(
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
private apiService: ApiService,
private vaultOnboardingService: VaultOnboardingServiceAbstraction,
private configService: ConfigService,
) {}
async ngOnInit() {
@@ -67,6 +72,9 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
this.setInstallExtLink();
this.individualVaultPolicyCheck();
this.checkForBrowserExtension();
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
async ngOnChanges(changes: SimpleChanges) {
@@ -162,7 +170,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
}
emitToAddCipher() {
this.onAddCipher.emit();
this.onAddCipher.emit(CipherType.Login);
}
setInstallExtLink() {

View File

@@ -6,14 +6,18 @@
[organizations]="allOrganizations"
[canCreateCollections]="canCreateCollections"
[collection]="selectedCollection"
(onAddCipher)="addCipher()"
(onAddCipher)="addCipher($event)"
(onAddCollection)="addCollection()"
(onAddFolder)="addFolder()"
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
></app-vault-header>
<app-vault-onboarding [ciphers]="ciphers" [orgs]="allOrganizations" (onAddCipher)="addCipher()">
<app-vault-onboarding
[ciphers]="ciphers"
[orgs]="allOrganizations"
(onAddCipher)="addCipher($event)"
>
</app-vault-onboarding>
<div class="tw-flex tw-flex-row -tw-mx-2.5">
@@ -52,7 +56,6 @@
[showAdminActions]="false"
[showBulkAddToCollections]="vaultBulkManagementActionEnabled$ | async"
(onEvent)="onVaultItemsEvent($event)"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async"
[vaultBulkManagementActionEnabled]="vaultBulkManagementActionEnabled$ | async"
>
</app-vault-items>
@@ -80,7 +83,7 @@
(click)="addCipher()"
*ngIf="filter.type !== 'trash'"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
<i class="bwi bwi-plus-f bwi-fw" aria-hidden="true"></i>
{{ "newItem" | i18n }}
</button>
</div>

View File

@@ -50,6 +50,7 @@ import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
@@ -157,13 +158,9 @@ export class VaultComponent implements OnInit, OnDestroy {
protected selectedCollection: TreeNode<CollectionView> | undefined;
protected canCreateCollections = false;
protected currentSearchText$: Observable<string>;
protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollectionsV1,
);
protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.VaultBulkManagementAction,
);
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>();
@@ -552,7 +549,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async shareCipher(cipher: CipherView) {
if ((await this.flexibleCollectionsV1Enabled()) && cipher.organizationId != null) {
if (cipher.organizationId != null) {
// You cannot move ciphers between organizations
this.showMissingPermissionsError();
return;
@@ -586,21 +583,27 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
async addCipher() {
async addCipher(cipherType?: CipherType) {
const component = await this.editCipher(null);
component.type = this.activeFilter.cipherType;
if (this.activeFilter.organizationId !== "MyVault") {
component.type = cipherType || this.activeFilter.cipherType;
if (
this.activeFilter.organizationId !== "MyVault" &&
this.activeFilter.organizationId != null
) {
component.organizationId = this.activeFilter.organizationId;
component.collections = (
await firstValueFrom(this.vaultFilterService.filteredCollections$)
).filter((c) => !c.readOnly && c.id != null);
}
const selectedColId = this.activeFilter.collectionId;
if (selectedColId !== "AllCollections") {
component.organizationId = component.collections.find(
(collection) => collection.id === selectedColId,
)?.organizationId;
component.collectionIds = [selectedColId];
if (selectedColId !== "AllCollections" && selectedColId != null) {
const selectedCollection = (
await firstValueFrom(this.vaultFilterService.filteredCollections$)
).find((c) => c.id === selectedColId);
component.organizationId = selectedCollection?.organizationId;
if (!selectedCollection.readOnly) {
component.collectionIds = [selectedColId];
}
}
component.folderId = this.activeFilter.folderId;
}
@@ -712,8 +715,7 @@ export class VaultComponent implements OnInit, OnDestroy {
async deleteCollection(collection: CollectionView): Promise<void> {
const organization = await this.organizationService.get(collection.organizationId);
const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$);
if (!collection.canDelete(organization, flexibleCollectionsV1Enabled)) {
if (!collection.canDelete(organization)) {
this.showMissingPermissionsError();
return;
}
@@ -811,7 +813,7 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
if ((await this.flexibleCollectionsV1Enabled()) && !c.edit) {
if (!c.edit) {
this.showMissingPermissionsError();
return;
}
@@ -834,7 +836,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async bulkRestore(ciphers: CipherView[]) {
if ((await this.flexibleCollectionsV1Enabled()) && ciphers.some((c) => !c.edit)) {
if (ciphers.some((c) => !c.edit)) {
this.showMissingPermissionsError();
return;
}
@@ -887,7 +889,7 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
if ((await this.flexibleCollectionsV1Enabled()) && !c.edit) {
if (!c.edit) {
this.showMissingPermissionsError();
return;
}
@@ -936,19 +938,12 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
const flexibleCollectionsV1Enabled = await this.flexibleCollectionsV1Enabled();
const canDeleteCollections =
collections == null ||
collections.every((c) =>
c.canDelete(
organizations.find((o) => o.id == c.organizationId),
flexibleCollectionsV1Enabled,
),
);
collections.every((c) => c.canDelete(organizations.find((o) => o.id == c.organizationId)));
const canDeleteCiphers = ciphers == null || ciphers.every((c) => c.edit);
if (flexibleCollectionsV1Enabled && (!canDeleteCollections || !canDeleteCiphers)) {
if (!canDeleteCollections || !canDeleteCiphers) {
this.showMissingPermissionsError();
return;
}
@@ -1052,10 +1047,7 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
if (
(await this.flexibleCollectionsV1Enabled()) &&
ciphers.some((c) => c.organizationId != null)
) {
if (ciphers.some((c) => c.organizationId != null)) {
// You cannot move ciphers between organizations
this.showMissingPermissionsError();
return;
@@ -1099,10 +1091,8 @@ export class VaultComponent implements OnInit, OnDestroy {
return true;
}
const flexibleCollectionsV1Enabled = await this.flexibleCollectionsV1Enabled();
const organization = this.allOrganizations.find((o) => o.id === cipher.organizationId);
return organization.canEditAllCiphers(flexibleCollectionsV1Enabled, false);
return organization.canEditAllCiphers(false);
}
private go(queryParams: any = null) {
@@ -1131,10 +1121,6 @@ export class VaultComponent implements OnInit, OnDestroy {
message: this.i18nService.t("missingPermissions"),
});
}
private flexibleCollectionsV1Enabled() {
return firstValueFrom(this.flexibleCollectionsV1Enabled$);
}
}
/**

View File

@@ -82,12 +82,7 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected loadCollections() {
if (
!this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
)
) {
if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) {
return super.loadCollections();
}
return Promise.resolve(this.collections);
@@ -98,10 +93,7 @@ export class AddEditComponent extends BaseAddEditComponent {
const firstCipherCheck = await super.loadCipher();
if (
!this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
) &&
!this.organization.canEditAllCiphers(this.restrictProviderAccess) &&
firstCipherCheck != null
) {
return firstCipherCheck;
@@ -116,24 +108,14 @@ export class AddEditComponent extends BaseAddEditComponent {
}
protected encryptCipher() {
if (
!this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
)
) {
if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) {
return super.encryptCipher();
}
return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher);
}
protected async deleteCipher() {
if (
!this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
)
) {
if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) {
return super.deleteCipher();
}
return this.cipher.isDeleted

View File

@@ -28,7 +28,6 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
viewOnly = false;
organization: Organization;
private flexibleCollectionsV1Enabled = false;
private restrictProviderAccess = false;
constructor(
@@ -60,9 +59,6 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
async ngOnInit() {
await super.ngOnInit();
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
);
this.restrictProviderAccess = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.RestrictProviderAccess),
);
@@ -70,10 +66,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
protected async reupload(attachment: AttachmentView) {
if (
this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
) &&
this.organization.canEditAllCiphers(this.restrictProviderAccess) &&
this.showFixOldAttachments(attachment)
) {
await super.reuploadCipherAttachment(attachment, true);
@@ -81,12 +74,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
}
protected async loadCipher() {
if (
!this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
)
) {
if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) {
return await super.loadCipher();
}
const response = await this.apiService.getCipherAdmin(this.cipherId);
@@ -97,20 +85,12 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
return this.cipherService.saveAttachmentWithServer(
this.cipherDomain,
file,
this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
),
this.organization.canEditAllCiphers(this.restrictProviderAccess),
);
}
protected deleteCipherAttachment(attachmentId: string) {
if (
!this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
)
) {
if (!this.organization.canEditAllCiphers(this.restrictProviderAccess)) {
return super.deleteCipherAttachment(attachmentId);
}
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
@@ -118,11 +98,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On
protected showFixOldAttachments(attachment: AttachmentView) {
return (
attachment.key == null &&
this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
)
attachment.key == null && this.organization.canEditAllCiphers(this.restrictProviderAccess)
);
}
}

View File

@@ -61,10 +61,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
protected async loadCipher() {
// if cipher is unassigned use apiService. We can see this by looking at this.collectionIds
if (
!this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
) &&
!this.organization.canEditAllCiphers(this.restrictProviderAccess) &&
this.collectionIds.length !== 0
) {
return await super.loadCipher();
@@ -89,10 +86,7 @@ export class CollectionsComponent extends BaseCollectionsComponent {
protected saveCollections() {
if (
this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccess,
) ||
this.organization.canEditAllCiphers(this.restrictProviderAccess) ||
this.collectionIds.length === 0
) {
const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds);

View File

@@ -87,7 +87,6 @@ export class VaultHeaderComponent implements OnInit {
protected CollectionDialogTabType = CollectionDialogTabType;
protected organizations$ = this.organizationService.organizations$;
protected flexibleCollectionsV1Enabled = false;
protected restrictProviderAccessFlag = false;
constructor(
@@ -100,9 +99,6 @@ export class VaultHeaderComponent implements OnInit {
) {}
async ngOnInit() {
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
);
this.restrictProviderAccessFlag = await this.configService.getFeatureFlag(
FeatureFlag.RestrictProviderAccess,
);
@@ -195,7 +191,7 @@ export class VaultHeaderComponent implements OnInit {
}
// Otherwise, check if we can edit the specified collection
return this.collection.node.canEdit(this.organization, this.flexibleCollectionsV1Enabled);
return this.collection.node.canEdit(this.organization);
}
addCipher() {
@@ -225,14 +221,11 @@ export class VaultHeaderComponent implements OnInit {
}
// Otherwise, check if we can delete the specified collection
return this.collection.node.canDelete(this.organization, this.flexibleCollectionsV1Enabled);
return this.collection.node.canDelete(this.organization);
}
get canViewCollectionInfo(): boolean {
return this.collection.node.canViewCollectionInfo(
this.organization,
this.flexibleCollectionsV1Enabled,
);
return this.collection.node.canViewCollectionInfo(this.organization);
}
get canCreateCollection(): boolean {

View File

@@ -68,39 +68,12 @@
[showBulkEditCollectionAccess]="true"
[showBulkAddToCollections]="true"
[viewingOrgVault]="true"
[flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled"
[addAccessStatus]="addAccessStatus$ | async"
[addAccessToggle]="showAddAccessToggle"
[restrictProviderAccess]="restrictProviderAccessEnabled"
>
</app-vault-items>
<ng-container *ngIf="!flexibleCollectionsV1Enabled">
<div
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
*ngIf="showMissingCollectionPermissionMessage"
>
<bit-icon [icon]="noItemIcon" aria-hidden="true"></bit-icon>
<p>{{ "noPermissionToViewAllCollectionItems" | i18n }}</p>
</div>
<div
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
*ngIf="isEmpty && !showMissingCollectionPermissionMessage && !performingInitialLoad"
>
<bit-icon [icon]="noItemIcon" aria-hidden="true"></bit-icon>
<p>{{ "noItemsInList" | i18n }}</p>
<button
type="button"
buttonType="primary"
bitButton
(click)="addCipher()"
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newItem" | i18n }}
</button>
</div></ng-container
>
<ng-container *ngIf="flexibleCollectionsV1Enabled && !performingInitialLoad && isEmpty">
<ng-container *ngIf="!performingInitialLoad && isEmpty">
<bit-no-items *ngIf="!showCollectionAccessRestricted">
<span slot="title" class="tw-mt-4 tw-block">{{ "noItemsInList" | i18n }}</span>
<button
@@ -116,15 +89,8 @@
</bit-no-items>
<collection-access-restricted
*ngIf="showCollectionAccessRestricted"
[canEditCollection]="
selectedCollection?.node?.canEdit(organization, flexibleCollectionsV1Enabled)
"
[canViewCollectionInfo]="
selectedCollection?.node?.canViewCollectionInfo(
organization,
flexibleCollectionsV1Enabled
)
"
[canEditCollection]="selectedCollection?.node?.canEdit(organization)"
[canViewCollectionInfo]="selectedCollection?.node?.canViewCollectionInfo(organization)"
(viewCollectionClicked)="
editCollection(selectedCollection.node, $event.tab, $event.readonly)
"

View File

@@ -150,12 +150,6 @@ export class VaultComponent implements OnInit, OnDestroy {
protected collections: CollectionAdminView[];
protected selectedCollection: TreeNode<CollectionAdminView> | undefined;
protected isEmpty: boolean;
/**
* Used to show an old missing permission message for custom users with DeleteAnyCollection
* @deprecated Replaced with showCollectionAccessRestricted$ and this should be removed after flexible collections V1
* is released
*/
protected showMissingCollectionPermissionMessage: boolean;
protected showCollectionAccessRestricted: boolean;
protected currentSearchText$: Observable<string>;
/**
@@ -164,16 +158,12 @@ export class VaultComponent implements OnInit, OnDestroy {
*/
protected editableCollections$: Observable<CollectionAdminView[]>;
protected allCollectionsWithoutUnassigned$: Observable<CollectionAdminView[]>;
private _flexibleCollectionsV1FlagEnabled: boolean;
protected get flexibleCollectionsV1Enabled(): boolean {
return this._flexibleCollectionsV1FlagEnabled;
}
protected orgRevokedUsers: OrganizationUserUserDetailsResponse[];
private _restrictProviderAccessFlagEnabled: boolean;
protected get restrictProviderAccessEnabled(): boolean {
return this._restrictProviderAccessFlagEnabled && this.flexibleCollectionsV1Enabled;
return this._restrictProviderAccessFlagEnabled;
}
protected get hideVaultFilters(): boolean {
@@ -228,10 +218,6 @@ export class VaultComponent implements OnInit, OnDestroy {
: "trashCleanupWarning",
);
this._flexibleCollectionsV1FlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.FlexibleCollectionsV1,
);
this._restrictProviderAccessFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.RestrictProviderAccess,
);
@@ -254,7 +240,7 @@ export class VaultComponent implements OnInit, OnDestroy {
switchMap(async ([organization]) => {
this.organization = organization;
if (!organization.canEditAnyCollection(this.flexibleCollectionsV1Enabled)) {
if (!organization.canEditAnyCollection) {
await this.syncService.fullSync(false);
}
@@ -327,12 +313,7 @@ export class VaultComponent implements OnInit, OnDestroy {
this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe(
map((collections) => {
// Users that can edit all ciphers can implicitly add to / edit within any collection
if (
this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccessEnabled,
)
) {
if (this.organization.canEditAllCiphers(this.restrictProviderAccessEnabled)) {
return collections;
}
// The user is only allowed to add/edit items to assigned collections that are not readonly
@@ -377,34 +358,12 @@ export class VaultComponent implements OnInit, OnDestroy {
return [];
}
if (this.flexibleCollectionsV1Enabled) {
// Flexible collections V1 logic.
// If the user can edit all ciphers for the organization then fetch them ALL.
if (
organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccessEnabled,
)
) {
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
} else {
// Otherwise, only fetch ciphers they have access to (includes unassigned for admins).
ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id);
}
// If the user can edit all ciphers for the organization then fetch them ALL.
if (organization.canEditAllCiphers(this.restrictProviderAccessEnabled)) {
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
} else {
// Pre-flexible collections logic, to be removed after flexible collections is fully released
if (
organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccessEnabled,
)
) {
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
} else {
ciphers = (await this.cipherService.getAllDecrypted()).filter(
(c) => c.organizationId === organization.id,
);
}
// Otherwise, only fetch ciphers they have access to (includes unassigned for admins).
ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id);
}
await this.searchService.indexCiphers(ciphers, organization.id);
@@ -469,9 +428,8 @@ export class VaultComponent implements OnInit, OnDestroy {
// Add access toggle is only shown if allowAdminAccessToAllCollectionItems is false and there are unmanaged collections the user can edit
this.showAddAccessToggle =
this.flexibleCollectionsV1Enabled &&
!this.organization.allowAdminAccessToAllCollectionItems &&
this.organization.canEditUnmanagedCollections() &&
this.organization.canEditUnmanagedCollections &&
collectionsToReturn.some((c) => c.unmanaged);
if (addAccessStatus === 1 && this.showAddAccessToggle) {
@@ -508,10 +466,7 @@ export class VaultComponent implements OnInit, OnDestroy {
return (
(filter.collectionId === Unassigned &&
!organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) ||
(!organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccessEnabled,
) &&
(!organization.canEditAllCiphers(this.restrictProviderAccessEnabled) &&
collection != undefined &&
!collection.node.assigned)
);
@@ -531,7 +486,7 @@ export class VaultComponent implements OnInit, OnDestroy {
return [];
}
if (this.flexibleCollectionsV1Enabled && showCollectionAccessRestricted) {
if (showCollectionAccessRestricted) {
// Do not show ciphers for restricted collections
// Ciphers belonging to multiple collections may still be present in $allCiphers and shouldn't be visible
return [];
@@ -548,25 +503,6 @@ export class VaultComponent implements OnInit, OnDestroy {
shareReplay({ refCount: true, bufferSize: 1 }),
);
const showMissingCollectionPermissionMessage$ = combineLatest([
filter$,
selectedCollection$,
organization$,
]).pipe(
map(([filter, collection, organization]) => {
return (
// Filtering by unassigned, show message if not admin
(filter.collectionId === Unassigned &&
!organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) ||
// Filtering by a collection, so show message if user is not assigned
(collection != undefined &&
!collection.node.assigned &&
!organization.canEditAnyCollection(this.flexibleCollectionsV1Enabled))
);
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
firstSetup$
.pipe(
switchMap(() => combineLatest([this.route.queryParams, organization$])),
@@ -576,17 +512,9 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
let canEditCipher: boolean;
if (this.flexibleCollectionsV1Enabled) {
canEditCipher =
organization.canEditAllCiphers(true, this.restrictProviderAccessEnabled) ||
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
} else {
canEditCipher =
organization.canEditAnyCollection(this.flexibleCollectionsV1Enabled) ||
(await this.cipherService.get(cipherId)) != null;
}
const canEditCipher =
organization.canEditAllCiphers(this.restrictProviderAccessEnabled) ||
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
if (canEditCipher) {
await this.editCipherId(cipherId);
@@ -646,7 +574,6 @@ export class VaultComponent implements OnInit, OnDestroy {
ciphers$,
collections$,
selectedCollection$,
showMissingCollectionPermissionMessage$,
showCollectionAccessRestricted$,
]),
),
@@ -661,7 +588,6 @@ export class VaultComponent implements OnInit, OnDestroy {
ciphers,
collections,
selectedCollection,
showMissingCollectionPermissionMessage,
showCollectionAccessRestricted,
]) => {
this.organization = organization;
@@ -671,7 +597,6 @@ export class VaultComponent implements OnInit, OnDestroy {
this.ciphers = ciphers;
this.collections = collections;
this.selectedCollection = selectedCollection;
this.showMissingCollectionPermissionMessage = showMissingCollectionPermissionMessage;
this.showCollectionAccessRestricted = showCollectionAccessRestricted;
this.isEmpty = collections?.length === 0 && ciphers?.length === 0;
@@ -812,32 +737,28 @@ export class VaultComponent implements OnInit, OnDestroy {
async editCipherCollections(cipher: CipherView) {
let collections: CollectionAdminView[] = [];
if (this.flexibleCollectionsV1Enabled) {
// V1 limits admins to only adding items to collections they have access to.
collections = await firstValueFrom(
this.allCollectionsWithoutUnassigned$.pipe(
map((c) => {
return c.sort((a, b) => {
if (
a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) &&
!b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled)
) {
return -1;
} else if (
!a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) &&
b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled)
) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
});
}),
),
);
} else {
collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$);
}
// Admins limited to only adding items to collections they have access to.
collections = await firstValueFrom(
this.allCollectionsWithoutUnassigned$.pipe(
map((c) => {
return c.sort((a, b) => {
if (
a.canEditItems(this.organization, this.restrictProviderAccessEnabled) &&
!b.canEditItems(this.organization, this.restrictProviderAccessEnabled)
) {
return -1;
} else if (
!a.canEditItems(this.organization, this.restrictProviderAccessEnabled) &&
b.canEditItems(this.organization, this.restrictProviderAccessEnabled)
) {
return 1;
} else {
return a.name.localeCompare(b.name);
}
});
}),
),
);
const dialog = openOrgVaultCollectionsDialog(this.dialogService, {
data: {
collectionIds: cipher.collectionIds,
@@ -855,14 +776,8 @@ export class VaultComponent implements OnInit, OnDestroy {
async addCipher() {
let collections: CollectionView[] = [];
if (this.flexibleCollectionsV1Enabled) {
// V1 limits admins to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
} else {
collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
(c) => !c.readOnly && c.id != Unassigned,
);
}
// Admins limited to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
await this.editCipher(null, (comp) => {
comp.type = this.activeFilter.cipherType;
@@ -954,14 +869,8 @@ export class VaultComponent implements OnInit, OnDestroy {
let collections: CollectionView[] = [];
if (this.flexibleCollectionsV1Enabled) {
// V1 limits admins to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
} else {
collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
(c) => !c.readOnly && c.id != Unassigned,
);
}
// Admins limited to only adding items to collections they have access to.
collections = await firstValueFrom(this.editableCollections$);
await this.editCipher(cipher, (comp) => {
comp.cloneMode = true;
@@ -977,7 +886,6 @@ export class VaultComponent implements OnInit, OnDestroy {
if (
!this.organization.permissions.editAnyCollection &&
this.flexibleCollectionsV1Enabled &&
!c.edit &&
!this.organization.allowAdminAccessToAllCollectionItems
) {
@@ -991,9 +899,7 @@ export class VaultComponent implements OnInit, OnDestroy {
// Allow restore of an Unassigned Item
try {
const asAdmin =
this.organization?.canEditAnyCollection(this.flexibleCollectionsV1Enabled) ||
c.isUnassigned;
const asAdmin = this.organization?.canEditAnyCollection || c.isUnassigned;
await this.cipherService.restoreWithServer(c.id, asAdmin);
this.toastService.showToast({
variant: "success",
@@ -1009,7 +915,6 @@ export class VaultComponent implements OnInit, OnDestroy {
async bulkRestore(ciphers: CipherView[]) {
if (
!this.organization.permissions.editAnyCollection &&
this.flexibleCollectionsV1Enabled &&
ciphers.some((c) => !c.edit && !this.organization.allowAdminAccessToAllCollectionItems)
) {
this.showMissingPermissionsError();
@@ -1025,10 +930,7 @@ export class VaultComponent implements OnInit, OnDestroy {
const unassignedCiphers: string[] = [];
// If user has edit all Access no need to check for unassigned ciphers
const canEditAll = this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccessEnabled,
);
const canEditAll = this.organization.canEditAllCiphers(this.restrictProviderAccessEnabled);
if (canEditAll) {
ciphers.map((cipher) => {
@@ -1069,14 +971,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async deleteCipher(c: CipherView): Promise<boolean> {
if (
this.flexibleCollectionsV1Enabled &&
!c.edit &&
!this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccessEnabled,
)
) {
if (!c.edit && !this.organization.canEditAllCiphers(this.restrictProviderAccessEnabled)) {
this.showMissingPermissionsError();
return;
}
@@ -1111,7 +1006,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async deleteCollection(collection: CollectionAdminView): Promise<void> {
if (!collection.canDelete(this.organization, this.flexibleCollectionsV1Enabled)) {
if (!collection.canDelete(this.organization)) {
this.showMissingPermissionsError();
return;
}
@@ -1178,17 +1073,13 @@ export class VaultComponent implements OnInit, OnDestroy {
}
const canDeleteCollections =
collections == null ||
collections.every((c) => c.canDelete(organization, this.flexibleCollectionsV1Enabled));
collections == null || collections.every((c) => c.canDelete(organization));
const canDeleteCiphers =
ciphers == null ||
ciphers.every((c) => c.edit) ||
this.organization.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccessEnabled,
);
this.organization.canEditAllCiphers(this.restrictProviderAccessEnabled);
if (this.flexibleCollectionsV1Enabled && (!canDeleteCiphers || !canDeleteCollections)) {
if (!canDeleteCiphers || !canDeleteCollections) {
this.showMissingPermissionsError();
return;
}
@@ -1268,9 +1159,7 @@ export class VaultComponent implements OnInit, OnDestroy {
data: {
organizationId: this.organization?.id,
parentCollectionId: this.selectedCollection?.node.id,
limitNestedCollections: !this.organization.canEditAnyCollection(
this.flexibleCollectionsV1Enabled,
),
limitNestedCollections: !this.organization.canEditAnyCollection,
},
});
@@ -1295,9 +1184,7 @@ export class VaultComponent implements OnInit, OnDestroy {
initialTab: tab,
readonly: readonly,
isAddAccessCollection: c.unmanaged,
limitNestedCollections: !this.organization.canEditAnyCollection(
this.flexibleCollectionsV1Enabled,
),
limitNestedCollections: !this.organization.canEditAnyCollection,
},
});
@@ -1335,10 +1222,7 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
if (
this.flexibleCollectionsV1Enabled &&
collections.some((c) => !c.canEdit(organization, this.flexibleCollectionsV1Enabled))
) {
if (collections.some((c) => !c.canEdit(organization))) {
this.showMissingPermissionsError();
return;
}
@@ -1366,15 +1250,7 @@ export class VaultComponent implements OnInit, OnDestroy {
return;
}
let availableCollections: CollectionView[];
if (this.flexibleCollectionsV1Enabled) {
availableCollections = await firstValueFrom(this.editableCollections$);
} else {
availableCollections = (
await firstValueFrom(this.vaultFilterService.filteredCollections$)
).filter((c) => c.id != Unassigned);
}
const availableCollections = await firstValueFrom(this.editableCollections$);
const dialog = AssignCollectionsWebComponent.open(this.dialogService, {
data: {
@@ -1405,10 +1281,7 @@ export class VaultComponent implements OnInit, OnDestroy {
protected deleteCipherWithServer(id: string, permanent: boolean, isUnassigned: boolean) {
const asAdmin =
this.organization?.canEditAllCiphers(
this.flexibleCollectionsV1Enabled,
this.restrictProviderAccessEnabled,
) || isUnassigned;
this.organization?.canEditAllCiphers(this.restrictProviderAccessEnabled) || isUnassigned;
return permanent
? this.cipherService.deleteWithServer(id, asAdmin)
: this.cipherService.softDeleteWithServer(id, asAdmin);