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:
@@ -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,
|
||||
};
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 }} ×
|
||||
{{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} /
|
||||
<strong>{{ "total" | i18n }}:</strong>
|
||||
{{ adjustSubscriptionForm.value.newSeatCount || 0 }} ×
|
||||
{{ 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 }} ×
|
||||
<strong>{{ "maxSeatCost" | i18n }}:</strong>
|
||||
{{ adjustSubscriptionForm.value.newMaxSeats || 0 }} ×
|
||||
{{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} /
|
||||
{{ interval | i18n }}</bit-hint
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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$);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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$);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
"
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user