[AC-1124] Restrict admins from accessing items in the Collections tab (#7537)
* [AC-1124] Add getManyFromApiForOrganization to cipher.service.ts * [AC-1124] Use getManyFromApiForOrganization when a user does not have access to all ciphers * [AC-1124] Vault changes - Show new collection access restricted view - Include unassigned ciphers for restricted admins - Restrict collections when creating/cloning/editing ciphers * [AC-1124] Update edit cipher on page navigation to check if user can access the cipher * [AC-1124] Hide ciphers from restricted collections * [AC-1124] Ensure providers are not shown collection access restricted view * [AC-1124] Modify add-edit component to call the correct endpoint when a restricted admin attempts to add-edit a cipher * [AC-1124] Fix bug after merge with main * [AC-1124] Use private this._organization * [AC-1124] Fix broken builds
This commit is contained in:
parent
3ee27fc61f
commit
5c6245aaae
|
@ -10,6 +10,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
@ -29,7 +30,7 @@ import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
|
|||
import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service";
|
||||
import { BrowserFido2UserInterfaceSession } from "../../../fido2/browser-fido2-user-interface.service";
|
||||
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
|
||||
import { VaultPopoutType, closeAddEditVaultItemPopout } from "../../utils/vault-popout-window";
|
||||
import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-add-edit",
|
||||
|
@ -66,6 +67,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
|
@ -85,6 +87,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||
dialogService,
|
||||
window,
|
||||
datePipe,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
|||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
@ -48,6 +49,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges,
|
|||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
|
@ -67,6 +69,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnChanges,
|
|||
dialogService,
|
||||
window,
|
||||
datePipe,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
@ -50,6 +51,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
|
|||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
|
@ -70,6 +72,7 @@ export class EmergencyAddEditCipherComponent extends BaseAddEditComponent {
|
|||
sendApiService,
|
||||
dialogService,
|
||||
datePipe,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve
|
|||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { EventType, ProductType } from "@bitwarden/common/enums";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
@ -62,6 +63,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
|
@ -81,6 +83,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
|
|||
dialogService,
|
||||
window,
|
||||
datePipe,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
|||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
@ -52,6 +53,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
datePipe: DatePipe,
|
||||
configService: ConfigServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
cipherService,
|
||||
|
@ -72,6 +74,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||
sendApiService,
|
||||
dialogService,
|
||||
datePipe,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -81,7 +84,9 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||
(this.ownershipOptions.length > 1 || !this.allowPersonal)
|
||||
) {
|
||||
if (this.organization != null) {
|
||||
return this.cloneMode && this.organization.canEditAnyCollection;
|
||||
return (
|
||||
this.cloneMode && this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)
|
||||
);
|
||||
} else {
|
||||
return !this.editMode || this.cloneMode;
|
||||
}
|
||||
|
@ -90,14 +95,14 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||
}
|
||||
|
||||
protected loadCollections() {
|
||||
if (!this.organization.canEditAnyCollection) {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
return super.loadCollections();
|
||||
}
|
||||
return Promise.resolve(this.collections);
|
||||
}
|
||||
|
||||
protected async loadCipher() {
|
||||
if (!this.organization.canEditAnyCollection) {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
return await super.loadCipher();
|
||||
}
|
||||
const response = await this.apiService.getCipherAdmin(this.cipherId);
|
||||
|
@ -110,14 +115,14 @@ export class AddEditComponent extends BaseAddEditComponent {
|
|||
}
|
||||
|
||||
protected encryptCipher() {
|
||||
if (!this.organization.canEditAnyCollection) {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
return super.encryptCipher();
|
||||
}
|
||||
return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher);
|
||||
}
|
||||
|
||||
protected async deleteCipher() {
|
||||
if (!this.organization.canEditAnyCollection) {
|
||||
if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
|
||||
return super.deleteCipher();
|
||||
}
|
||||
return this.cipher.isDeleted
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components";
|
||||
|
||||
|
@ -13,9 +13,10 @@ const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="140" height=
|
|||
selector: "collection-access-restricted",
|
||||
standalone: true,
|
||||
imports: [SharedModule, ButtonModule, NoItemsModule],
|
||||
template: `<bit-no-items [icon]="icon">
|
||||
template: `<bit-no-items [icon]="icon" class="tw-mt-2 tw-block">
|
||||
<span slot="title" class="tw-mt-4 tw-block">{{ "collectionAccessRestricted" | i18n }}</span>
|
||||
<button
|
||||
*ngIf="canEdit"
|
||||
slot="button"
|
||||
bitButton
|
||||
(click)="editInfoClicked.emit()"
|
||||
|
@ -29,5 +30,7 @@ const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="140" height=
|
|||
export class CollectionAccessRestrictedComponent {
|
||||
protected icon = icon;
|
||||
|
||||
@Input() canEdit = false;
|
||||
|
||||
@Output() editInfoClicked = new EventEmitter<void>();
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ import { VaultFilterComponent as BaseVaultFilterComponent } from "../../individu
|
|||
import { VaultFilterService } from "../../individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import {
|
||||
VaultFilterList,
|
||||
VaultFilterType,
|
||||
VaultFilterSection,
|
||||
VaultFilterType,
|
||||
} from "../../individual-vault/vault-filter/shared/models/vault-filter-section.type";
|
||||
import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
|
|
|
@ -61,30 +61,53 @@
|
|||
[viewingOrgVault]="true"
|
||||
>
|
||||
</app-vault-items>
|
||||
<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"
|
||||
<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"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "newItem" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<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">
|
||||
<bit-no-items *ngIf="isEmpty && !showCollectionAccessRestricted">
|
||||
<span slot="title" class="tw-mt-4 tw-block">{{ "noItemsInList" | i18n }}</span>
|
||||
<button
|
||||
slot="button"
|
||||
bitButton
|
||||
(click)="addCipher()"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus"></i> {{ "newItem" | i18n }}
|
||||
</button>
|
||||
</bit-no-items>
|
||||
<collection-access-restricted
|
||||
*ngIf="showCollectionAccessRestricted"
|
||||
[canEdit]="selectedCollection != null && selectedCollection.node.canEdit(organization)"
|
||||
(editInfoClicked)="editCollection(selectedCollection.node, CollectionDialogTabType.Info)"
|
||||
>
|
||||
</collection-access-restricted>
|
||||
</ng-container>
|
||||
<div
|
||||
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
|
||||
*ngIf="performingInitialLoad"
|
||||
|
|
|
@ -127,13 +127,20 @@ 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>;
|
||||
protected editableCollections$: Observable<CollectionView[]>;
|
||||
protected showBulkEditCollectionAccess$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.BulkCollectionAccess,
|
||||
false,
|
||||
);
|
||||
protected flexibleCollectionsEnabled: boolean;
|
||||
protected flexibleCollectionsV1Enabled: boolean;
|
||||
|
||||
private searchText$ = new Subject<string>();
|
||||
private refresh$ = new BehaviorSubject<void>(null);
|
||||
|
@ -176,6 +183,11 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
: "trashCleanupWarning",
|
||||
);
|
||||
|
||||
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
false,
|
||||
);
|
||||
|
||||
const filter$ = this.routedVaultFilterService.filter$;
|
||||
const organizationId$ = filter$.pipe(
|
||||
map((filter) => filter.organizationId),
|
||||
|
@ -259,6 +271,22 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
this.editableCollections$ = allCollectionsWithoutUnassigned$.pipe(
|
||||
map((collections) => {
|
||||
if (
|
||||
this.organization.canEditAnyCollection &&
|
||||
this.organization.allowAdminAccessToAllCollectionItems
|
||||
) {
|
||||
return collections;
|
||||
}
|
||||
if (this.organization.isProviderUser) {
|
||||
return collections;
|
||||
}
|
||||
return collections.filter((c) => c.assigned && !c.readOnly);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
const allCollections$ = combineLatest([organizationId$, allCollectionsWithoutUnassigned$]).pipe(
|
||||
map(([organizationId, allCollections]) => {
|
||||
const noneCollection = new CollectionAdminView();
|
||||
|
@ -277,32 +305,35 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
const allCiphers$ = organization$.pipe(
|
||||
concatMap(async (organization) => {
|
||||
let ciphers;
|
||||
if (organization.canEditAnyCollection) {
|
||||
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
|
||||
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||
(c) => c.organizationId === organization.id,
|
||||
);
|
||||
// Pre-flexible collections logic, to be removed after flexible collections is fully released
|
||||
if (organization.canEditAnyCollection) {
|
||||
ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id);
|
||||
} else {
|
||||
ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||
(c) => c.organizationId === organization.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.searchService.indexCiphers(ciphers, organization.id);
|
||||
|
||||
this.searchService.indexCiphers(ciphers, organization.id);
|
||||
return ciphers;
|
||||
}),
|
||||
);
|
||||
|
||||
const ciphers$ = combineLatest([allCiphers$, filter$, this.currentSearchText$]).pipe(
|
||||
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
||||
concatMap(async ([ciphers, filter, searchText]) => {
|
||||
if (filter.collectionId === undefined && filter.type === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filterFunction = createFilterFunction(filter);
|
||||
|
||||
if (this.searchService.isSearchable(searchText)) {
|
||||
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
|
||||
}
|
||||
|
||||
return ciphers.filter(filterFunction);
|
||||
const allCipherMap$ = allCiphers$.pipe(
|
||||
map((ciphers) => {
|
||||
return Object.fromEntries(ciphers.map((c) => [c.id, c]));
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
@ -364,6 +395,52 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
const showCollectionAccessRestricted$ = combineLatest([
|
||||
filter$,
|
||||
selectedCollection$,
|
||||
organization$,
|
||||
]).pipe(
|
||||
map(([filter, collection, organization]) => {
|
||||
return (
|
||||
(filter.collectionId === Unassigned && !organization.canUseAdminCollections) ||
|
||||
(!organization.allowAdminAccessToAllCollectionItems &&
|
||||
!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) &&
|
||||
collection != undefined &&
|
||||
!collection.node.assigned)
|
||||
);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
const ciphers$ = combineLatest([
|
||||
allCiphers$,
|
||||
filter$,
|
||||
this.currentSearchText$,
|
||||
showCollectionAccessRestricted$,
|
||||
]).pipe(
|
||||
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
||||
concatMap(async ([ciphers, filter, searchText, showCollectionAccessRestricted]) => {
|
||||
if (filter.collectionId === undefined && filter.type === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.flexibleCollectionsV1Enabled && 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 [];
|
||||
}
|
||||
|
||||
const filterFunction = createFilterFunction(filter);
|
||||
|
||||
if (this.searchService.isSearchable(searchText)) {
|
||||
return await this.searchService.searchCiphers(searchText, [filterFunction], ciphers);
|
||||
}
|
||||
|
||||
return ciphers.filter(filterFunction);
|
||||
}),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
const showMissingCollectionPermissionMessage$ = combineLatest([
|
||||
filter$,
|
||||
selectedCollection$,
|
||||
|
@ -390,23 +467,28 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
if (!cipherId) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
// Handle users with implicit collection access since they use the admin endpoint
|
||||
organization.canUseAdminCollections ||
|
||||
(await this.cipherService.get(cipherId)) != null
|
||||
) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.editCipherId(cipherId);
|
||||
|
||||
let canEditCipher: boolean;
|
||||
|
||||
if (this.flexibleCollectionsV1Enabled) {
|
||||
canEditCipher =
|
||||
organization.canEditAllCiphers(true) ||
|
||||
(await firstValueFrom(allCipherMap$))[cipherId] != undefined;
|
||||
} else {
|
||||
canEditCipher =
|
||||
organization.canUseAdminCollections ||
|
||||
(await this.cipherService.get(cipherId)) != null;
|
||||
}
|
||||
|
||||
if (canEditCipher) {
|
||||
await this.editCipherId(cipherId);
|
||||
} else {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
this.i18nService.t("unknownCipher"),
|
||||
);
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([], {
|
||||
await this.router.navigate([], {
|
||||
queryParams: { cipherId: null, itemId: null },
|
||||
queryParamsHandling: "merge",
|
||||
});
|
||||
|
@ -461,6 +543,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
collections$,
|
||||
selectedCollection$,
|
||||
showMissingCollectionPermissionMessage$,
|
||||
showCollectionAccessRestricted$,
|
||||
]),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
|
@ -475,6 +558,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
collections,
|
||||
selectedCollection,
|
||||
showMissingCollectionPermissionMessage,
|
||||
showCollectionAccessRestricted,
|
||||
]) => {
|
||||
this.organization = organization;
|
||||
this.filter = filter;
|
||||
|
@ -484,6 +568,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
this.collections = collections;
|
||||
this.selectedCollection = selectedCollection;
|
||||
this.showMissingCollectionPermissionMessage = showMissingCollectionPermissionMessage;
|
||||
this.showCollectionAccessRestricted = showCollectionAccessRestricted;
|
||||
|
||||
this.isEmpty = collections?.length === 0 && ciphers?.length === 0;
|
||||
|
||||
// This is a temporary fix to avoid double fetching collections.
|
||||
|
@ -591,13 +677,22 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
async editCipherCollections(cipher: CipherView) {
|
||||
const currCollections = await firstValueFrom(this.vaultFilterService.filteredCollections$);
|
||||
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,
|
||||
);
|
||||
}
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
CollectionsComponent,
|
||||
this.collectionsModalRef,
|
||||
(comp) => {
|
||||
comp.collectionIds = cipher.collectionIds;
|
||||
comp.collections = currCollections.filter((c) => !c.readOnly && c.id != Unassigned);
|
||||
comp.collections = collections;
|
||||
comp.organization = this.organization;
|
||||
comp.cipherId = cipher.id;
|
||||
comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
|
@ -609,9 +704,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
async addCipher() {
|
||||
const collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
|
||||
(c) => !c.readOnly && c.id != Unassigned,
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
await this.editCipher(null, (comp) => {
|
||||
comp.type = this.activeFilter.cipherType;
|
||||
|
@ -701,9 +803,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
const collections = (await firstValueFrom(this.vaultFilterService.filteredCollections$)).filter(
|
||||
(c) => !c.readOnly && c.id != Unassigned,
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
await this.editCipher(cipher, (comp) => {
|
||||
comp.cloneMode = true;
|
||||
|
@ -1008,6 +1117,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly CollectionDialogTabType = CollectionDialogTabType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { BreadcrumbsModule } from "@bitwarden/components";
|
||||
import { BreadcrumbsModule, NoItemsModule } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule } from "../../shared/loose-components.module";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
@ -31,6 +31,7 @@ import { VaultComponent } from "./vault.component";
|
|||
VaultItemsModule,
|
||||
CollectionDialogModule,
|
||||
CollectionAccessRestrictedComponent,
|
||||
NoItemsModule,
|
||||
],
|
||||
declarations: [VaultComponent, VaultHeaderComponent],
|
||||
exports: [VaultComponent],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DatePipe } from "@angular/common";
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { Observable, Subject, takeUntil, concatMap } from "rxjs";
|
||||
import { concatMap, Observable, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
|
@ -12,6 +12,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
|||
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
@ -22,7 +24,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
|
|||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SecureNoteType, UriMatchType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherType, SecureNoteType, UriMatchType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||
|
@ -87,6 +89,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||
private personalOwnershipPolicyAppliesToActiveUser: boolean;
|
||||
private previousCipherId: string;
|
||||
|
||||
protected flexibleCollectionsV1Enabled = false;
|
||||
|
||||
get fido2CredentialCreationDateValue(): string {
|
||||
const dateCreated = this.i18nService.t("dateCreated");
|
||||
const creationDate = this.datePipe.transform(
|
||||
|
@ -114,6 +118,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||
protected dialogService: DialogService,
|
||||
protected win: Window,
|
||||
protected datePipe: DatePipe,
|
||||
protected configService: ConfigServiceAbstraction,
|
||||
) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
||||
|
@ -174,6 +179,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.FlexibleCollectionsV1,
|
||||
false,
|
||||
);
|
||||
this.writeableCollections = await this.loadCollections();
|
||||
this.canUseReprompt = await this.passwordRepromptService.enabled();
|
||||
|
||||
|
@ -650,7 +659,13 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||
|
||||
protected saveCipher(cipher: Cipher) {
|
||||
const isNotClone = this.editMode && !this.cloneMode;
|
||||
const orgAdmin = this.organization?.isAdmin;
|
||||
let orgAdmin = this.organization?.isAdmin;
|
||||
|
||||
if (this.flexibleCollectionsV1Enabled) {
|
||||
// Flexible Collections V1 restricts admins, check the organization setting via canEditAllCiphers
|
||||
orgAdmin = this.organization?.canEditAllCiphers(true);
|
||||
}
|
||||
|
||||
return this.cipher.id == null
|
||||
? this.cipherService.createWithServer(cipher, orgAdmin)
|
||||
: this.cipherService.updateWithServer(cipher, orgAdmin, isNotClone);
|
||||
|
|
|
@ -196,6 +196,20 @@ export class Organization {
|
|||
return this.canEditAnyCollection;
|
||||
}
|
||||
|
||||
canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) {
|
||||
// Before Flexible Collections, anyone with editAnyCollection permission could edit all ciphers
|
||||
if (!flexibleCollectionsV1Enabled) {
|
||||
return this.canEditAnyCollection;
|
||||
}
|
||||
// Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins
|
||||
// Providers are not affected by allowAdminAccessToAllCollectionItems flag
|
||||
// note: canEditAnyCollection may change in the V1 to also ignore the allowAdminAccessToAllCollectionItems flag
|
||||
return (
|
||||
this.isProviderUser ||
|
||||
(this.allowAdminAccessToAllCollectionItems && this.canEditAnyCollection)
|
||||
);
|
||||
}
|
||||
|
||||
get canDeleteAnyCollection() {
|
||||
return this.isAdmin || this.permissions.deleteAnyCollection;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,11 @@ export abstract class CipherService {
|
|||
defaultMatch?: UriMatchType,
|
||||
) => Promise<CipherView[]>;
|
||||
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
|
||||
/**
|
||||
* Gets ciphers belonging to the specified organization that the user has explicit collection level access to.
|
||||
* Ciphers that are not assigned to any collections are only included for users with admin access.
|
||||
*/
|
||||
getManyFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
|
||||
getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
|
||||
getLastLaunchedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>;
|
||||
getNextCipherForUrl: (url: string) => Promise<CipherView>;
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ApiService } from "../../abstractions/api.service";
|
|||
import { SearchService } from "../../abstractions/search.service";
|
||||
import { SettingsService } from "../../abstractions/settings.service";
|
||||
import { ErrorResponse } from "../../models/response/error.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { View } from "../../models/view/view";
|
||||
import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction";
|
||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||
|
@ -387,6 +388,24 @@ export class CipherService implements CipherServiceAbstraction {
|
|||
|
||||
async getAllFromApiForOrganization(organizationId: string): Promise<CipherView[]> {
|
||||
const response = await this.apiService.getCiphersOrganization(organizationId);
|
||||
return await this.decryptOrganizationCiphersResponse(response, organizationId);
|
||||
}
|
||||
|
||||
async getManyFromApiForOrganization(organizationId: string): Promise<CipherView[]> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/ciphers/organization-details/assigned?organizationId=" + organizationId,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return this.decryptOrganizationCiphersResponse(response, organizationId);
|
||||
}
|
||||
|
||||
private async decryptOrganizationCiphersResponse(
|
||||
response: ListResponse<CipherResponse>,
|
||||
organizationId: string,
|
||||
): Promise<CipherView[]> {
|
||||
if (response?.data == null || response.data.length < 1) {
|
||||
return [];
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue