mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
[AC-1373] Flexible Collections (#6336)
* [AC-1117] Add manage permission (#5910) * Add 'manage' option to collection access permissions * Add 'manage' to collection permissions * remove service accidentally committed from another branch * Update CLI commands * update message casing to be consistent * access selector model updates * [AC-1374] Limit collection create/delete (#5963) * feat: udate request/response/data/domain models for new column, refs AC-1374 * feat: create collection management ui, refs AC-1374 * fix: remove limitCollectionCdOwnerAdmin boolean from org update request, refs AC-1374 * fix: moved collection management UI, removed comments, refs AC-1374 * fix: observable chaining now properly calls API when local org updated, refs AC-1374 * fix: remove unused form template variables, refs AC-1374 * fix: clean up observable chain, refs AC-1374 * fix: remove parent.parent route, refs AC-1374 * fix: add cd explaination, refs AC-1374 * [AC-1649] Remove organizationId from collection-bulk-delete.request (#6343) * refactor: remove organizationId from collection-bulk-delete-request, refs AC-1649 * refactor: remove request model from dialog component, refs AC-1649 * [AC-1174] Bulk collection management (#6133) * [AC-1174] Add bulk edit collection access event type * [AC-1174] Add bulk edit collection access menu option * [AC-1174] Add initial bulk collections access dialog * [AC-1174] Add logic to open bulk edit collections dialog * [AC-1174] Move AccessItemView helper methods to access selector model to be shared * [AC-1174] Add access selector to bulk collections dialog * [AC-1174] Add bulk assign access method to collection-admin service * [AC-1174] Introduce strongly typed BulkCollectionAccessRequest model * [AC-1174] Update vault item event type name * Update DialogService dependency --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * Rename LimitCollectionCdOwnerAdmin -> LimitCollectionCreationDeletion (#6409) * Add manage property to synced Collection data * Revert "Add manage property to synced Collection data" Pushed to feature branch instead of a new one This reverts commit65cd39589c
. * Add manage property to synced Collection data * Revert "Add manage property to synced Collection data" This reverts commitf7fa30b79a
. * [AC-1680] Add manage property to collection view and response models (#6417) * Add manage property to synced Collection data * Update tests * feat: add LimitCollectionCreationDeletion conditional to canCreateNewCollections logic, refs AC-1659 (#6429) * [AC-1669] Enforce Can Manage permission on Collection dialog (#6493) * [AC-1669] Cleanup unhandled promise warnings * [AC-1669] Force change detection to ensure AccessSelector has the most recent items * [AC-1669] Initially select acting member when creating a new collection * [AC-1669] Add validator to ensure manage permission is selected * [AC-1669] Update error toast logic to support access tab errors * [AC-1669] Add error icon * [AC-1713] [Flexible collections] Add feature flags to clients (#6486) * Add FlexibleCollections and BulkCollectionAccess flags * Flag Collection Management settings * Flag bulk collection access dialog * Flag collection access modal changes * [AC-1662] Add LimitCollecitonCreationDeletion conditional to CanDelete logic (#6526) * feat: implement limitCollectionCreationDeletion into canDelete logic, refs AC-1662 * feat: make canDelete functions backwards compatible with feature flag, refs AC-1662 * feat: update vault-items.component for async getter, refs AC-1662 * feat: update configService injection, refs AC-1662 * feat: add config service to canDelete reference, refs AC-1662 * fix: remove configservice dependency from views, refs AC-1757 (#6686) * Add missing provider to vault-items.stories (#6690) * Fix imports after update from master --------- Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com> Co-authored-by: Shane Melton <smelton@bitwarden.com>
This commit is contained in:
parent
2ec3f808d2
commit
0c3b569d0e
@ -1,15 +1,17 @@
|
|||||||
export class SelectionReadOnly {
|
export class SelectionReadOnly {
|
||||||
static template(): SelectionReadOnly {
|
static template(): SelectionReadOnly {
|
||||||
return new SelectionReadOnly("00000000-0000-0000-0000-000000000000", false, false);
|
return new SelectionReadOnly("00000000-0000-0000-0000-000000000000", false, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
hidePasswords: boolean;
|
hidePasswords: boolean;
|
||||||
|
manage: boolean;
|
||||||
|
|
||||||
constructor(id: string, readOnly: boolean, hidePasswords: boolean) {
|
constructor(id: string, readOnly: boolean, hidePasswords: boolean, manage: boolean) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.readOnly = readOnly;
|
this.readOnly = readOnly;
|
||||||
this.hidePasswords = hidePasswords || false;
|
this.hidePasswords = hidePasswords || false;
|
||||||
|
this.manage = manage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,9 @@ export class EditCommand {
|
|||||||
const groups =
|
const groups =
|
||||||
req.groups == null
|
req.groups == null
|
||||||
? null
|
? null
|
||||||
: req.groups.map((g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords));
|
: req.groups.map(
|
||||||
|
(g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage)
|
||||||
|
);
|
||||||
const request = new CollectionRequest();
|
const request = new CollectionRequest();
|
||||||
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
|
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
|
||||||
request.externalId = req.externalId;
|
request.externalId = req.externalId;
|
||||||
|
@ -427,7 +427,9 @@ export class GetCommand extends DownloadCommand {
|
|||||||
const groups =
|
const groups =
|
||||||
response.groups == null
|
response.groups == null
|
||||||
? null
|
? null
|
||||||
: response.groups.map((g) => new SelectionReadOnly(g.id, g.readOnly, g.hidePasswords));
|
: response.groups.map(
|
||||||
|
(g) => new SelectionReadOnly(g.id, g.readOnly, g.hidePasswords, g.manage)
|
||||||
|
);
|
||||||
const res = new OrganizationCollectionResponse(decCollection, groups);
|
const res = new OrganizationCollectionResponse(decCollection, groups);
|
||||||
return Response.success(res);
|
return Response.success(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -184,7 +184,9 @@ export class CreateCommand {
|
|||||||
const groups =
|
const groups =
|
||||||
req.groups == null
|
req.groups == null
|
||||||
? null
|
? null
|
||||||
: req.groups.map((g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords));
|
: req.groups.map(
|
||||||
|
(g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage)
|
||||||
|
);
|
||||||
const request = new CollectionRequest();
|
const request = new CollectionRequest();
|
||||||
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
|
request.name = (await this.cryptoService.encrypt(req.name, orgKey)).encryptedString;
|
||||||
request.externalId = req.externalId;
|
request.externalId = req.externalId;
|
||||||
|
@ -76,7 +76,7 @@ export class InternalGroupService extends GroupService {
|
|||||||
request.accessAll = group.accessAll;
|
request.accessAll = group.accessAll;
|
||||||
request.users = group.members;
|
request.users = group.members;
|
||||||
request.collections = group.collections.map(
|
request.collections = group.collections.map(
|
||||||
(c) => new SelectionReadOnlyRequest(c.id, c.readOnly, c.hidePasswords)
|
(c) => new SelectionReadOnlyRequest(c.id, c.readOnly, c.hidePasswords, c.manage)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (group.id == undefined) {
|
if (group.id == undefined) {
|
||||||
|
@ -80,6 +80,7 @@ export class UserAdminService {
|
|||||||
id: c.id,
|
id: c.id,
|
||||||
hidePasswords: c.hidePasswords,
|
hidePasswords: c.hidePasswords,
|
||||||
readOnly: c.readOnly,
|
readOnly: c.readOnly,
|
||||||
|
manage: c.manage,
|
||||||
}));
|
}));
|
||||||
view.groups = u.groups;
|
view.groups = u.groups;
|
||||||
view.accessSecretsManager = u.accessSecretsManager;
|
view.accessSecretsManager = u.accessSecretsManager;
|
||||||
|
@ -4,12 +4,14 @@ interface SelectionResponseLike {
|
|||||||
id: string;
|
id: string;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
hidePasswords: boolean;
|
hidePasswords: boolean;
|
||||||
|
manage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CollectionAccessSelectionView extends View {
|
export class CollectionAccessSelectionView extends View {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly readOnly: boolean;
|
readonly readOnly: boolean;
|
||||||
readonly hidePasswords: boolean;
|
readonly hidePasswords: boolean;
|
||||||
|
readonly manage: boolean;
|
||||||
|
|
||||||
constructor(response?: SelectionResponseLike) {
|
constructor(response?: SelectionResponseLike) {
|
||||||
super();
|
super();
|
||||||
@ -21,5 +23,6 @@ export class CollectionAccessSelectionView extends View {
|
|||||||
this.id = response.id;
|
this.id = response.id;
|
||||||
this.readOnly = response.readOnly;
|
this.readOnly = response.readOnly;
|
||||||
this.hidePasswords = response.hidePasswords;
|
this.hidePasswords = response.hidePasswords;
|
||||||
|
this.manage = response.manage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
[columnHeader]="'member' | i18n"
|
[columnHeader]="'member' | i18n"
|
||||||
[selectorLabelText]="'selectMembers' | i18n"
|
[selectorLabelText]="'selectMembers' | i18n"
|
||||||
[emptySelectionText]="'noMembersAdded' | i18n"
|
[emptySelectionText]="'noMembersAdded' | i18n"
|
||||||
|
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||||
></bit-access-selector>
|
></bit-access-selector>
|
||||||
</bit-tab>
|
</bit-tab>
|
||||||
|
|
||||||
@ -60,6 +61,7 @@
|
|||||||
[columnHeader]="'collection' | i18n"
|
[columnHeader]="'collection' | i18n"
|
||||||
[selectorLabelText]="'selectCollections' | i18n"
|
[selectorLabelText]="'selectCollections' | i18n"
|
||||||
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
||||||
|
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||||
></bit-access-selector>
|
></bit-access-selector>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-tab>
|
</bit-tab>
|
||||||
|
@ -5,7 +5,9 @@ import { catchError, combineLatest, from, map, of, Subject, switchMap, takeUntil
|
|||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -78,6 +80,11 @@ export const openGroupAddEditDialog = (
|
|||||||
templateUrl: "group-add-edit.component.html",
|
templateUrl: "group-add-edit.component.html",
|
||||||
})
|
})
|
||||||
export class GroupAddEditComponent implements OnInit, OnDestroy {
|
export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
|
protected flexibleCollectionsEnabled$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.FlexibleCollections,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
protected PermissionMode = PermissionMode;
|
protected PermissionMode = PermissionMode;
|
||||||
protected ResultType = GroupAddEditDialogResultType;
|
protected ResultType = GroupAddEditDialogResultType;
|
||||||
|
|
||||||
@ -181,7 +188,8 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dialogService: DialogService
|
private dialogService: DialogService,
|
||||||
|
private configService: ConfigServiceAbstraction
|
||||||
) {
|
) {
|
||||||
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
||||||
}
|
}
|
||||||
|
@ -289,6 +289,7 @@
|
|||||||
[columnHeader]="'groups' | i18n"
|
[columnHeader]="'groups' | i18n"
|
||||||
[selectorLabelText]="'selectGroups' | i18n"
|
[selectorLabelText]="'selectGroups' | i18n"
|
||||||
[emptySelectionText]="'noGroupsAdded' | i18n"
|
[emptySelectionText]="'noGroupsAdded' | i18n"
|
||||||
|
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||||
></bit-access-selector>
|
></bit-access-selector>
|
||||||
</bit-tab>
|
</bit-tab>
|
||||||
<bit-tab [label]="'collections' | i18n">
|
<bit-tab [label]="'collections' | i18n">
|
||||||
@ -321,6 +322,7 @@
|
|||||||
[columnHeader]="'collection' | i18n"
|
[columnHeader]="'collection' | i18n"
|
||||||
[selectorLabelText]="'selectCollections' | i18n"
|
[selectorLabelText]="'selectCollections' | i18n"
|
||||||
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
||||||
|
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||||
></bit-access-selector
|
></bit-access-selector
|
||||||
></bit-tab>
|
></bit-tab>
|
||||||
</bit-tab-group>
|
</bit-tab-group>
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
} from "@bitwarden/common/admin-console/enums";
|
} from "@bitwarden/common/admin-console/enums";
|
||||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
@ -64,6 +66,11 @@ export enum MemberDialogResult {
|
|||||||
templateUrl: "member-dialog.component.html",
|
templateUrl: "member-dialog.component.html",
|
||||||
})
|
})
|
||||||
export class MemberDialogComponent implements OnInit, OnDestroy {
|
export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||||
|
protected flexibleCollectionsEnabled$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.FlexibleCollections,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
editMode = false;
|
editMode = false;
|
||||||
isRevoked = false;
|
isRevoked = false;
|
||||||
@ -134,7 +141,8 @@ export class MemberDialogComponent implements OnInit, OnDestroy {
|
|||||||
private groupService: GroupService,
|
private groupService: GroupService,
|
||||||
private userService: UserAdminService,
|
private userService: UserAdminService,
|
||||||
private organizationUserService: OrganizationUserService,
|
private organizationUserService: OrganizationUserService,
|
||||||
private dialogService: DialogService
|
private dialogService: DialogService,
|
||||||
|
private configService: ConfigServiceAbstraction
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
></i>
|
></i>
|
||||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
<form *ngIf="org && !loading" #form [bitSubmit]="submit" [formGroup]="formGroup">
|
<form *ngIf="org && !loading" [bitSubmit]="submit" [formGroup]="formGroup">
|
||||||
<div class="tw-grid tw-grid-cols-2 tw-gap-5">
|
<div class="tw-grid tw-grid-cols-2 tw-gap-5">
|
||||||
<div>
|
<div>
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
@ -52,6 +52,27 @@
|
|||||||
{{ "rotateApiKey" | i18n }}
|
{{ "rotateApiKey" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<form
|
||||||
|
*ngIf="org && !loading && (showCollectionManagementSettings$ | async)"
|
||||||
|
[bitSubmit]="submitCollectionManagement"
|
||||||
|
[formGroup]="collectionManagementFormGroup"
|
||||||
|
>
|
||||||
|
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "collectionManagement" | i18n }}</h1>
|
||||||
|
<p>{{ "collectionManagementDesc" | i18n }}</p>
|
||||||
|
<bit-form-control>
|
||||||
|
<bit-label>{{ "limitCollectionCreationDeletionDesc" | i18n }}</bit-label>
|
||||||
|
<input type="checkbox" bitCheckbox formControlName="limitCollectionCreationDeletion" />
|
||||||
|
</bit-form-control>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
bitButton
|
||||||
|
bitFormButton
|
||||||
|
buttonType="primary"
|
||||||
|
id="collectionManagementSubmitButton"
|
||||||
|
>
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5 !tw-text-danger">{{ "dangerZone" | i18n }}</h1>
|
<h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5 !tw-text-danger">{{ "dangerZone" | i18n }}</h1>
|
||||||
<div class="tw-rounded tw-border tw-border-solid tw-border-danger-500 tw-bg-background tw-p-5">
|
<div class="tw-rounded tw-border tw-border-solid tw-border-danger-500 tw-bg-background tw-p-5">
|
||||||
<p>{{ "dangerZoneDesc" | i18n }}</p>
|
<p>{{ "dangerZoneDesc" | i18n }}</p>
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { combineLatest, lastValueFrom, Subject, switchMap, takeUntil, from } from "rxjs";
|
import { combineLatest, lastValueFrom, Subject, switchMap, takeUntil, from, of } from "rxjs";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { OrganizationCollectionManagementUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-collection-management-update.request";
|
||||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
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 { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-update.request";
|
||||||
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
|
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
@ -38,8 +40,11 @@ export class AccountComponent {
|
|||||||
loading = true;
|
loading = true;
|
||||||
canUseApi = false;
|
canUseApi = false;
|
||||||
org: OrganizationResponse;
|
org: OrganizationResponse;
|
||||||
formPromise: Promise<OrganizationResponse>;
|
|
||||||
taxFormPromise: Promise<unknown>;
|
taxFormPromise: Promise<unknown>;
|
||||||
|
showCollectionManagementSettings$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.FlexibleCollections,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
// FormGroup validators taken from server Organization domain object
|
// FormGroup validators taken from server Organization domain object
|
||||||
protected formGroup = this.formBuilder.group({
|
protected formGroup = this.formBuilder.group({
|
||||||
@ -60,6 +65,10 @@ export class AccountComponent {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
protected collectionManagementFormGroup = this.formBuilder.group({
|
||||||
|
limitCollectionCreationDeletion: [false],
|
||||||
|
});
|
||||||
|
|
||||||
protected organizationId: string;
|
protected organizationId: string;
|
||||||
protected publicKeyBuffer: Uint8Array;
|
protected publicKeyBuffer: Uint8Array;
|
||||||
|
|
||||||
@ -71,27 +80,27 @@ export class AccountComponent {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private logService: LogService,
|
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private formBuilder: FormBuilder
|
private formBuilder: FormBuilder,
|
||||||
|
private configService: ConfigServiceAbstraction
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||||
|
|
||||||
this.route.parent.parent.params
|
this.route.params
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((params) => {
|
switchMap((params) => this.organizationService.get$(params.organizationId)),
|
||||||
|
switchMap((organization) => {
|
||||||
return combineLatest([
|
return combineLatest([
|
||||||
// Organization domain
|
of(organization),
|
||||||
this.organizationService.get$(params.organizationId),
|
|
||||||
// OrganizationResponse for form population
|
// OrganizationResponse for form population
|
||||||
from(this.organizationApiService.get(params.organizationId)),
|
from(this.organizationApiService.get(organization.id)),
|
||||||
// Organization Public Key
|
// Organization Public Key
|
||||||
from(this.organizationApiService.getKeys(params.organizationId)),
|
from(this.organizationApiService.getKeys(organization.id)),
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
takeUntil(this.destroy$)
|
takeUntil(this.destroy$)
|
||||||
@ -102,6 +111,16 @@ export class AccountComponent {
|
|||||||
this.canEditSubscription = organization.canEditSubscription;
|
this.canEditSubscription = organization.canEditSubscription;
|
||||||
this.canUseApi = organization.useApi;
|
this.canUseApi = organization.useApi;
|
||||||
|
|
||||||
|
// Update disabled states - reactive forms prefers not using disabled attribute
|
||||||
|
if (!this.selfHosted) {
|
||||||
|
this.formGroup.get("orgName").enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.selfHosted || this.canEditSubscription) {
|
||||||
|
this.formGroup.get("billingEmail").enable();
|
||||||
|
this.formGroup.get("businessName").enable();
|
||||||
|
}
|
||||||
|
|
||||||
// Org Response
|
// Org Response
|
||||||
this.org = orgResponse;
|
this.org = orgResponse;
|
||||||
|
|
||||||
@ -114,16 +133,9 @@ export class AccountComponent {
|
|||||||
billingEmail: this.org.billingEmail,
|
billingEmail: this.org.billingEmail,
|
||||||
businessName: this.org.businessName,
|
businessName: this.org.businessName,
|
||||||
});
|
});
|
||||||
|
this.collectionManagementFormGroup.patchValue({
|
||||||
// Update disabled states - reactive forms prefers not using disabled attribute
|
limitCollectionCreationDeletion: this.org.limitCollectionCreationDeletion,
|
||||||
if (!this.selfHosted) {
|
});
|
||||||
this.formGroup.get("orgName").enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.selfHosted || this.canEditSubscription) {
|
|
||||||
this.formGroup.get("billingEmail").enable();
|
|
||||||
this.formGroup.get("businessName").enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
});
|
});
|
||||||
@ -153,11 +165,25 @@ export class AccountComponent {
|
|||||||
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formPromise = this.organizationApiService.save(this.organizationId, request);
|
await this.organizationApiService.save(this.organizationId, request);
|
||||||
await this.formPromise;
|
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("organizationUpdated"));
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("organizationUpdated"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
submitCollectionManagement = async () => {
|
||||||
|
const request = new OrganizationCollectionManagementUpdateRequest();
|
||||||
|
request.limitCreateDeleteOwnerAdmin =
|
||||||
|
this.collectionManagementFormGroup.value.limitCollectionCreationDeletion;
|
||||||
|
|
||||||
|
await this.organizationApiService.updateCollectionManagement(this.organizationId, request);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("collectionManagementUpdated")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
async deleteOrganization() {
|
async deleteOrganization() {
|
||||||
const dialog = openDeleteOrganizationDialog(this.dialogService, {
|
const dialog = openDeleteOrganizationDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
|
@ -122,6 +122,10 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
|
|||||||
{ perm: CollectionPermission.Edit, labelId: "canEdit" },
|
{ perm: CollectionPermission.Edit, labelId: "canEdit" },
|
||||||
{ perm: CollectionPermission.EditExceptPass, labelId: "canEditExceptPass" },
|
{ perm: CollectionPermission.EditExceptPass, labelId: "canEditExceptPass" },
|
||||||
];
|
];
|
||||||
|
private canManagePermissionListItem = {
|
||||||
|
perm: CollectionPermission.Manage,
|
||||||
|
labelId: "canManage",
|
||||||
|
};
|
||||||
protected initialPermission = CollectionPermission.View;
|
protected initialPermission = CollectionPermission.View;
|
||||||
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@ -192,6 +196,11 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
|
|||||||
*/
|
*/
|
||||||
@Input() showGroupColumn: boolean;
|
@Input() showGroupColumn: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable Flexible Collections changes (feature flag)
|
||||||
|
*/
|
||||||
|
@Input() flexibleCollectionsEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly formBuilder: FormBuilder,
|
private readonly formBuilder: FormBuilder,
|
||||||
private readonly i18nService: I18nService
|
private readonly i18nService: I18nService
|
||||||
@ -254,7 +263,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
|
|||||||
this.pauseChangeNotification = false;
|
this.pauseChangeNotification = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
async ngOnInit() {
|
||||||
// Watch the internal formArray for changes and propagate them
|
// Watch the internal formArray for changes and propagate them
|
||||||
this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => {
|
this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => {
|
||||||
if (!this.notifyOnChange || this.pauseChangeNotification) {
|
if (!this.notifyOnChange || this.pauseChangeNotification) {
|
||||||
@ -268,6 +277,10 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
|
|||||||
}
|
}
|
||||||
this.notifyOnChange(v);
|
this.notifyOnChange(v);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.flexibleCollectionsEnabled) {
|
||||||
|
this.permissionList.push(this.canManagePermissionListItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
|
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
|
||||||
import {
|
import {
|
||||||
OrganizationUserStatusType,
|
OrganizationUserStatusType,
|
||||||
OrganizationUserType,
|
OrganizationUserType,
|
||||||
} from "@bitwarden/common/admin-console/enums";
|
} from "@bitwarden/common/admin-console/enums";
|
||||||
import { SelectItemView } from "@bitwarden/components";
|
import { SelectItemView } from "@bitwarden/components";
|
||||||
|
|
||||||
import { CollectionAccessSelectionView } from "../../../core";
|
import { CollectionAccessSelectionView, GroupView } from "../../../core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permission options that replace/correspond with readOnly and hidePassword server fields.
|
* Permission options that replace/correspond with manage, readOnly, and hidePassword server fields.
|
||||||
*/
|
*/
|
||||||
export enum CollectionPermission {
|
export enum CollectionPermission {
|
||||||
View = "view",
|
View = "view",
|
||||||
ViewExceptPass = "viewExceptPass",
|
ViewExceptPass = "viewExceptPass",
|
||||||
Edit = "edit",
|
Edit = "edit",
|
||||||
EditExceptPass = "editExceptPass",
|
EditExceptPass = "editExceptPass",
|
||||||
|
Manage = "manage",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AccessItemType {
|
export enum AccessItemType {
|
||||||
@ -82,7 +84,9 @@ export type AccessItemValue = {
|
|||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
export const convertToPermission = (value: CollectionAccessSelectionView) => {
|
export const convertToPermission = (value: CollectionAccessSelectionView) => {
|
||||||
if (value.readOnly) {
|
if (value.manage) {
|
||||||
|
return CollectionPermission.Manage;
|
||||||
|
} else if (value.readOnly) {
|
||||||
return value.hidePasswords ? CollectionPermission.ViewExceptPass : CollectionPermission.View;
|
return value.hidePasswords ? CollectionPermission.ViewExceptPass : CollectionPermission.View;
|
||||||
} else {
|
} else {
|
||||||
return value.hidePasswords ? CollectionPermission.EditExceptPass : CollectionPermission.Edit;
|
return value.hidePasswords ? CollectionPermission.EditExceptPass : CollectionPermission.Edit;
|
||||||
@ -91,7 +95,7 @@ export const convertToPermission = (value: CollectionAccessSelectionView) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an AccessItemValue back into a CollectionAccessView class using the CollectionPermission
|
* Converts an AccessItemValue back into a CollectionAccessView class using the CollectionPermission
|
||||||
* to determine the values for `readOnly` and `hidePassword`
|
* to determine the values for `manage`, `readOnly`, and `hidePassword`
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
export const convertToSelectionView = (value: AccessItemValue) => {
|
export const convertToSelectionView = (value: AccessItemValue) => {
|
||||||
@ -99,6 +103,7 @@ export const convertToSelectionView = (value: AccessItemValue) => {
|
|||||||
id: value.id,
|
id: value.id,
|
||||||
readOnly: readOnly(value.permission),
|
readOnly: readOnly(value.permission),
|
||||||
hidePasswords: hidePassword(value.permission),
|
hidePasswords: hidePassword(value.permission),
|
||||||
|
manage: value.permission === CollectionPermission.Manage,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,3 +112,29 @@ const readOnly = (perm: CollectionPermission) =>
|
|||||||
|
|
||||||
const hidePassword = (perm: CollectionPermission) =>
|
const hidePassword = (perm: CollectionPermission) =>
|
||||||
[CollectionPermission.ViewExceptPass, CollectionPermission.EditExceptPass].includes(perm);
|
[CollectionPermission.ViewExceptPass, CollectionPermission.EditExceptPass].includes(perm);
|
||||||
|
|
||||||
|
export function mapGroupToAccessItemView(group: GroupView): AccessItemView {
|
||||||
|
return {
|
||||||
|
id: group.id,
|
||||||
|
type: AccessItemType.Group,
|
||||||
|
listName: group.name,
|
||||||
|
labelName: group.name,
|
||||||
|
accessAllItems: group.accessAll,
|
||||||
|
readonly: group.accessAll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Use view when user apis are migrated to a service
|
||||||
|
export function mapUserToAccessItemView(user: OrganizationUserUserDetailsResponse): AccessItemView {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
type: AccessItemType.Member,
|
||||||
|
email: user.email,
|
||||||
|
role: user.type,
|
||||||
|
listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email,
|
||||||
|
labelName: user.name ?? user.email,
|
||||||
|
status: user.status,
|
||||||
|
accessAllItems: user.accessAll,
|
||||||
|
readonly: user.accessAll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -64,6 +64,12 @@
|
|||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
</bit-tab>
|
</bit-tab>
|
||||||
<bit-tab label="{{ 'access' | i18n }}">
|
<bit-tab label="{{ 'access' | i18n }}">
|
||||||
|
<div
|
||||||
|
class="tw-mb-3 tw-text-danger"
|
||||||
|
*ngIf="formGroup.controls.access.hasError('managePermissionRequired')"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-error"></i> {{ "managePermissionRequired" | i18n }}
|
||||||
|
</div>
|
||||||
<bit-access-selector
|
<bit-access-selector
|
||||||
*ngIf="organization.useGroups"
|
*ngIf="organization.useGroups"
|
||||||
[permissionMode]="PermissionMode.Edit"
|
[permissionMode]="PermissionMode.Edit"
|
||||||
@ -73,6 +79,7 @@
|
|||||||
[selectorLabelText]="'selectGroupsAndMembers' | i18n"
|
[selectorLabelText]="'selectGroupsAndMembers' | i18n"
|
||||||
[selectorHelpText]="'userPermissionOverrideHelper' | i18n"
|
[selectorHelpText]="'userPermissionOverrideHelper' | i18n"
|
||||||
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
|
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
|
||||||
|
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||||
></bit-access-selector>
|
></bit-access-selector>
|
||||||
<bit-access-selector
|
<bit-access-selector
|
||||||
*ngIf="!organization.useGroups"
|
*ngIf="!organization.useGroups"
|
||||||
@ -82,6 +89,7 @@
|
|||||||
[columnHeader]="'memberColumnHeader' | i18n"
|
[columnHeader]="'memberColumnHeader' | i18n"
|
||||||
[selectorLabelText]="'selectMembers' | i18n"
|
[selectorLabelText]="'selectMembers' | i18n"
|
||||||
[emptySelectionText]="'noMembersAdded' | i18n"
|
[emptySelectionText]="'noMembersAdded' | i18n"
|
||||||
|
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||||
></bit-access-selector>
|
></bit-access-selector>
|
||||||
</bit-tab>
|
</bit-tab>
|
||||||
</bit-tab-group>
|
</bit-tab-group>
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { AbstractControl, FormBuilder, Validators } from "@angular/forms";
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
|
firstValueFrom,
|
||||||
|
from,
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
of,
|
of,
|
||||||
@ -14,23 +16,27 @@ import {
|
|||||||
|
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses";
|
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CollectionResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
import { CollectionResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { DialogService, BitValidators } from "@bitwarden/components";
|
import { BitValidators, DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { GroupService, GroupView } from "../../../admin-console/organizations/core";
|
import { GroupService, GroupView } from "../../../admin-console/organizations/core";
|
||||||
import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component";
|
import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component";
|
||||||
import {
|
import {
|
||||||
AccessItemView,
|
|
||||||
AccessItemValue,
|
|
||||||
AccessItemType,
|
AccessItemType,
|
||||||
convertToSelectionView,
|
AccessItemValue,
|
||||||
|
AccessItemView,
|
||||||
|
CollectionPermission,
|
||||||
convertToPermission,
|
convertToPermission,
|
||||||
|
convertToSelectionView,
|
||||||
|
mapGroupToAccessItemView,
|
||||||
|
mapUserToAccessItemView,
|
||||||
} from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
|
} from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
|
||||||
import { CollectionAdminService } from "../../core/collection-admin.service";
|
import { CollectionAdminService } from "../../core/collection-admin.service";
|
||||||
import { CollectionAdminView } from "../../core/views/collection-admin.view";
|
import { CollectionAdminView } from "../../core/views/collection-admin.view";
|
||||||
@ -64,6 +70,11 @@ export enum CollectionDialogAction {
|
|||||||
templateUrl: "collection-dialog.component.html",
|
templateUrl: "collection-dialog.component.html",
|
||||||
})
|
})
|
||||||
export class CollectionDialogComponent implements OnInit, OnDestroy {
|
export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||||
|
protected flexibleCollectionsEnabled$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.FlexibleCollections,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
protected organizations$: Observable<Organization[]>;
|
protected organizations$: Observable<Organization[]>;
|
||||||
|
|
||||||
@ -94,7 +105,9 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private organizationUserService: OrganizationUserService,
|
private organizationUserService: OrganizationUserService,
|
||||||
private dialogService: DialogService
|
private dialogService: DialogService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private configService: ConfigServiceAbstraction
|
||||||
) {
|
) {
|
||||||
this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info;
|
this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info;
|
||||||
}
|
}
|
||||||
@ -118,7 +131,11 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
// Opened from the org vault
|
// Opened from the org vault
|
||||||
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
|
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
|
||||||
this.loadOrg(this.params.organizationId, this.params.collectionIds);
|
await this.loadOrg(this.params.organizationId, this.params.collectionIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await firstValueFrom(this.flexibleCollectionsEnabled$)) {
|
||||||
|
this.formGroup.controls.access.addValidators(validateCanManagePermission);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,51 +156,76 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
organization: organization$,
|
organization: organization$,
|
||||||
collections: this.collectionService.getAll(orgId),
|
collections: this.collectionService.getAll(orgId),
|
||||||
collectionDetails: this.params.collectionId
|
collectionDetails: this.params.collectionId
|
||||||
? this.collectionService.get(orgId, this.params.collectionId)
|
? from(this.collectionService.get(orgId, this.params.collectionId))
|
||||||
: of(null),
|
: of(null),
|
||||||
groups: groups$,
|
groups: groups$,
|
||||||
users: this.organizationUserService.getAllUsers(orgId),
|
users: this.organizationUserService.getAllUsers(orgId),
|
||||||
|
flexibleCollections: this.flexibleCollectionsEnabled$,
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
|
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
|
||||||
.subscribe(({ organization, collections, collectionDetails, groups, users }) => {
|
.subscribe(
|
||||||
this.organization = organization;
|
({ organization, collections, collectionDetails, groups, users, flexibleCollections }) => {
|
||||||
this.accessItems = [].concat(
|
this.organization = organization;
|
||||||
groups.map(mapGroupToAccessItemView),
|
this.accessItems = [].concat(
|
||||||
users.data.map(mapUserToAccessItemView)
|
groups.map(mapGroupToAccessItemView),
|
||||||
);
|
users.data.map(mapUserToAccessItemView)
|
||||||
|
);
|
||||||
|
|
||||||
if (collectionIds) {
|
// Force change detection to update the access selector's items
|
||||||
collections = collections.filter((c) => collectionIds.includes(c.id));
|
this.changeDetectorRef.detectChanges();
|
||||||
}
|
|
||||||
|
|
||||||
if (this.params.collectionId) {
|
if (collectionIds) {
|
||||||
this.collection = collections.find((c) => c.id === this.collectionId);
|
collections = collections.filter((c) => collectionIds.includes(c.id));
|
||||||
this.nestOptions = collections.filter((c) => c.id !== this.collectionId);
|
|
||||||
|
|
||||||
if (!this.collection) {
|
|
||||||
throw new Error("Could not find collection to edit.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, parent } = parseName(this.collection);
|
if (this.params.collectionId) {
|
||||||
if (parent !== undefined && !this.nestOptions.find((c) => c.name === parent)) {
|
this.collection = collections.find((c) => c.id === this.collectionId);
|
||||||
this.deletedParentName = parent;
|
this.nestOptions = collections.filter((c) => c.id !== this.collectionId);
|
||||||
|
|
||||||
|
if (!this.collection) {
|
||||||
|
throw new Error("Could not find collection to edit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, parent } = parseName(this.collection);
|
||||||
|
if (parent !== undefined && !this.nestOptions.find((c) => c.name === parent)) {
|
||||||
|
this.deletedParentName = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessSelections = mapToAccessSelections(collectionDetails);
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
name,
|
||||||
|
externalId: this.collection.externalId,
|
||||||
|
parent,
|
||||||
|
access: accessSelections,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.nestOptions = collections;
|
||||||
|
const parent = collections.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: flexibleCollections
|
||||||
|
? CollectionPermission.Manage
|
||||||
|
: CollectionPermission.Edit,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
parent: parent?.name ?? undefined,
|
||||||
|
access: initialSelection,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessSelections = mapToAccessSelections(collectionDetails);
|
this.loading = false;
|
||||||
this.formGroup.patchValue({
|
|
||||||
name,
|
|
||||||
externalId: this.collection.externalId,
|
|
||||||
parent,
|
|
||||||
access: accessSelections,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.nestOptions = collections;
|
|
||||||
const parent = collections.find((c) => c.id === this.params.parentCollectionId);
|
|
||||||
this.formGroup.patchValue({ parent: parent?.name ?? undefined });
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
this.loading = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get collectionId() {
|
protected get collectionId() {
|
||||||
@ -202,12 +244,20 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
this.formGroup.markAllAsTouched();
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
if (this.formGroup.invalid) {
|
if (this.formGroup.invalid) {
|
||||||
if (this.tabIndex === CollectionDialogTabType.Access) {
|
const accessTabError = this.formGroup.controls.access.hasError("managePermissionRequired");
|
||||||
|
|
||||||
|
if (this.tabIndex === CollectionDialogTabType.Access && !accessTabError) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
null,
|
null,
|
||||||
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("collectionInfo"))
|
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("collectionInfo"))
|
||||||
);
|
);
|
||||||
|
} else if (this.tabIndex === CollectionDialogTabType.Info && accessTabError) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("access"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -284,32 +334,6 @@ function parseName(collection: CollectionView) {
|
|||||||
return { name, parent };
|
return { name, parent };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapGroupToAccessItemView(group: GroupView): AccessItemView {
|
|
||||||
return {
|
|
||||||
id: group.id,
|
|
||||||
type: AccessItemType.Group,
|
|
||||||
listName: group.name,
|
|
||||||
labelName: group.name,
|
|
||||||
accessAllItems: group.accessAll,
|
|
||||||
readonly: group.accessAll,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use view when user apis are migrated to a service
|
|
||||||
function mapUserToAccessItemView(user: OrganizationUserUserDetailsResponse): AccessItemView {
|
|
||||||
return {
|
|
||||||
id: user.id,
|
|
||||||
type: AccessItemType.Member,
|
|
||||||
email: user.email,
|
|
||||||
role: user.type,
|
|
||||||
listName: user.name?.length > 0 ? `${user.name} (${user.email})` : user.email,
|
|
||||||
labelName: user.name ?? user.email,
|
|
||||||
status: user.status,
|
|
||||||
accessAllItems: user.accessAll,
|
|
||||||
readonly: user.accessAll,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapToAccessSelections(collectionDetails: CollectionAdminView): AccessItemValue[] {
|
function mapToAccessSelections(collectionDetails: CollectionAdminView): AccessItemValue[] {
|
||||||
if (collectionDetails == undefined) {
|
if (collectionDetails == undefined) {
|
||||||
return [];
|
return [];
|
||||||
@ -328,6 +352,16 @@ function mapToAccessSelections(collectionDetails: CollectionAdminView): AccessIt
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to ensure that at least one access item has Manage permission
|
||||||
|
*/
|
||||||
|
function validateCanManagePermission(control: AbstractControl) {
|
||||||
|
const access = control.value as AccessItemValue[];
|
||||||
|
const hasManagePermission = access.some((a) => a.permission === CollectionPermission.Manage);
|
||||||
|
|
||||||
|
return hasManagePermission ? null : { managePermissionRequired: true };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strongly typed helper to open a CollectionDialog
|
* Strongly typed helper to open a CollectionDialog
|
||||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||||
|
@ -6,6 +6,7 @@ import { VaultItem } from "./vault-item";
|
|||||||
export type VaultItemEvent =
|
export type VaultItemEvent =
|
||||||
| { type: "viewAttachments"; item: CipherView }
|
| { type: "viewAttachments"; item: CipherView }
|
||||||
| { type: "viewCollections"; item: CipherView }
|
| { type: "viewCollections"; item: CipherView }
|
||||||
|
| { type: "bulkEditCollectionAccess"; items: CollectionView[] }
|
||||||
| { type: "viewCollectionAccess"; item: CollectionView }
|
| { type: "viewCollectionAccess"; item: CollectionView }
|
||||||
| { type: "viewEvents"; item: CipherView }
|
| { type: "viewEvents"; item: CipherView }
|
||||||
| { type: "editCollection"; item: CollectionView }
|
| { type: "editCollection"; item: CollectionView }
|
||||||
|
@ -34,6 +34,15 @@
|
|||||||
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
|
||||||
{{ "moveSelected" | i18n }}
|
{{ "moveSelected" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
*ngIf="showAdminActions && showBulkEditCollectionAccess"
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="bulkEditCollectionAccess()"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||||
|
{{ "access" | i18n }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
*ngIf="bulkMoveAllowed"
|
*ngIf="bulkMoveAllowed"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -2,6 +2,8 @@ import { SelectionModel } from "@angular/cdk/collections";
|
|||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { TableDataSource } from "@bitwarden/components";
|
import { TableDataSource } from "@bitwarden/components";
|
||||||
@ -27,6 +29,8 @@ const MaxSelectionCount = 500;
|
|||||||
export class VaultItemsComponent {
|
export class VaultItemsComponent {
|
||||||
protected RowHeight = RowHeight;
|
protected RowHeight = RowHeight;
|
||||||
|
|
||||||
|
private flexibleCollectionsEnabled: boolean;
|
||||||
|
|
||||||
@Input() disabled: boolean;
|
@Input() disabled: boolean;
|
||||||
@Input() showOwner: boolean;
|
@Input() showOwner: boolean;
|
||||||
@Input() showCollections: boolean;
|
@Input() showCollections: boolean;
|
||||||
@ -36,9 +40,12 @@ export class VaultItemsComponent {
|
|||||||
@Input() showPremiumFeatures: boolean;
|
@Input() showPremiumFeatures: boolean;
|
||||||
@Input() showBulkMove: boolean;
|
@Input() showBulkMove: boolean;
|
||||||
@Input() showBulkTrashOptions: boolean;
|
@Input() showBulkTrashOptions: boolean;
|
||||||
|
// Encompasses functionality only available from the organization vault context
|
||||||
|
@Input() showAdminActions: boolean;
|
||||||
@Input() allOrganizations: Organization[] = [];
|
@Input() allOrganizations: Organization[] = [];
|
||||||
@Input() allCollections: CollectionView[] = [];
|
@Input() allCollections: CollectionView[] = [];
|
||||||
@Input() allGroups: GroupView[] = [];
|
@Input() allGroups: GroupView[] = [];
|
||||||
|
@Input() showBulkEditCollectionAccess = false;
|
||||||
|
|
||||||
private _ciphers?: CipherView[] = [];
|
private _ciphers?: CipherView[] = [];
|
||||||
@Input() get ciphers(): CipherView[] {
|
@Input() get ciphers(): CipherView[] {
|
||||||
@ -64,6 +71,14 @@ export class VaultItemsComponent {
|
|||||||
protected dataSource = new TableDataSource<VaultItem>();
|
protected dataSource = new TableDataSource<VaultItem>();
|
||||||
protected selection = new SelectionModel<VaultItem>(true, [], true);
|
protected selection = new SelectionModel<VaultItem>(true, [], true);
|
||||||
|
|
||||||
|
constructor(private configService: ConfigServiceAbstraction) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.FlexibleCollections
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get showExtraColumn() {
|
get showExtraColumn() {
|
||||||
return this.showCollections || this.showGroups || this.showOwner;
|
return this.showCollections || this.showGroups || this.showOwner;
|
||||||
}
|
}
|
||||||
@ -101,7 +116,7 @@ export class VaultItemsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
|
||||||
return collection.canDelete(organization);
|
return collection.canDelete(organization, this.flexibleCollectionsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toggleAll() {
|
protected toggleAll() {
|
||||||
@ -168,4 +183,13 @@ export class VaultItemsComponent {
|
|||||||
);
|
);
|
||||||
this.dataSource.data = items;
|
this.dataSource.data = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected bulkEditCollectionAccess() {
|
||||||
|
this.event({
|
||||||
|
type: "bulkEditCollectionAccess",
|
||||||
|
items: this.selection.selected
|
||||||
|
.filter((item) => item.collection !== undefined)
|
||||||
|
.map((item) => item.collection),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { SettingsService } from "@bitwarden/common/abstractions/settings.service
|
|||||||
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
@ -92,6 +93,15 @@ export default {
|
|||||||
},
|
},
|
||||||
} as Partial<TokenService>,
|
} as Partial<TokenService>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: ConfigServiceAbstraction,
|
||||||
|
useValue: {
|
||||||
|
getFeatureFlag() {
|
||||||
|
// does not currently affect any display logic, default all to OFF
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
applicationConfig({
|
applicationConfig({
|
||||||
@ -289,6 +299,7 @@ function createCollectionView(i: number): CollectionAdminView {
|
|||||||
id: group.id,
|
id: group.id,
|
||||||
hidePasswords: false,
|
hidePasswords: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
manage: false,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||||
|
|
||||||
|
export class BulkCollectionAccessRequest {
|
||||||
|
collectionIds: string[];
|
||||||
|
users: SelectionReadOnlyRequest[];
|
||||||
|
groups: SelectionReadOnlyRequest[];
|
||||||
|
}
|
@ -10,6 +10,9 @@ import {
|
|||||||
CollectionResponse,
|
CollectionResponse,
|
||||||
} from "@bitwarden/common/vault/models/response/collection.response";
|
} from "@bitwarden/common/vault/models/response/collection.response";
|
||||||
|
|
||||||
|
import { CollectionAccessSelectionView } from "../../admin-console/organizations/core";
|
||||||
|
|
||||||
|
import { BulkCollectionAccessRequest } from "./bulk-collection-access.request";
|
||||||
import { CollectionAdminView } from "./views/collection-admin.view";
|
import { CollectionAdminView } from "./views/collection-admin.view";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -68,6 +71,30 @@ export class CollectionAdminService {
|
|||||||
await this.apiService.deleteCollection(organizationId, collectionId);
|
await this.apiService.deleteCollection(organizationId, collectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bulkAssignAccess(
|
||||||
|
organizationId: string,
|
||||||
|
collectionIds: string[],
|
||||||
|
users: CollectionAccessSelectionView[],
|
||||||
|
groups: CollectionAccessSelectionView[]
|
||||||
|
): Promise<void> {
|
||||||
|
const request = new BulkCollectionAccessRequest();
|
||||||
|
request.collectionIds = collectionIds;
|
||||||
|
request.users = users.map(
|
||||||
|
(u) => new SelectionReadOnlyRequest(u.id, u.readOnly, u.hidePasswords, u.manage)
|
||||||
|
);
|
||||||
|
request.groups = groups.map(
|
||||||
|
(g) => new SelectionReadOnlyRequest(g.id, g.readOnly, g.hidePasswords, g.manage)
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
`organizations/${organizationId}/collections/bulk-access`,
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async decryptMany(
|
private async decryptMany(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
collections: CollectionResponse[] | CollectionAccessDetailsResponse[]
|
collections: CollectionResponse[] | CollectionAccessDetailsResponse[]
|
||||||
@ -105,10 +132,12 @@ export class CollectionAdminService {
|
|||||||
collection.externalId = model.externalId;
|
collection.externalId = model.externalId;
|
||||||
collection.name = (await this.cryptoService.encrypt(model.name, key)).encryptedString;
|
collection.name = (await this.cryptoService.encrypt(model.name, key)).encryptedString;
|
||||||
collection.groups = model.groups.map(
|
collection.groups = model.groups.map(
|
||||||
(group) => new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords)
|
(group) =>
|
||||||
|
new SelectionReadOnlyRequest(group.id, group.readOnly, group.hidePasswords, group.manage)
|
||||||
);
|
);
|
||||||
collection.users = model.users.map(
|
collection.users = model.users.map(
|
||||||
(user) => new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords)
|
(user) =>
|
||||||
|
new SelectionReadOnlyRequest(user.id, user.readOnly, user.hidePasswords, user.manage)
|
||||||
);
|
);
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,11 @@ export class CollectionAdminView extends CollectionView {
|
|||||||
return org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
|
return org?.canEditAnyCollection || (org?.canEditAssignedCollections && this.assigned);
|
||||||
}
|
}
|
||||||
|
|
||||||
override canDelete(org: Organization): boolean {
|
override canDelete(org: Organization, flexibleCollectionsEnabled: boolean): boolean {
|
||||||
return org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned);
|
if (flexibleCollectionsEnabled) {
|
||||||
|
return org?.canDeleteAnyCollection;
|
||||||
|
} else {
|
||||||
|
return org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ import { Component, Inject } from "@angular/core";
|
|||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { CollectionBulkDeleteRequest } from "@bitwarden/common/models/request/collection-bulk-delete.request";
|
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@ -139,11 +138,7 @@ export class BulkDeleteDialogComponent {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const deleteRequest = new CollectionBulkDeleteRequest(
|
return await this.apiService.deleteManyCollections(this.organization.id, this.collectionIds);
|
||||||
this.collectionIds,
|
|
||||||
this.organization.id
|
|
||||||
);
|
|
||||||
return await this.apiService.deleteManyCollections(deleteRequest);
|
|
||||||
// From individual vault, so there can be multiple organizations
|
// From individual vault, so there can be multiple organizations
|
||||||
} else if (this.organizations && this.collections) {
|
} else if (this.organizations && this.collections) {
|
||||||
const deletePromises: Promise<any>[] = [];
|
const deletePromises: Promise<any>[] = [];
|
||||||
@ -159,8 +154,9 @@ export class BulkDeleteDialogComponent {
|
|||||||
const orgCollections = this.collections
|
const orgCollections = this.collections
|
||||||
.filter((o) => o.organizationId === organization.id)
|
.filter((o) => o.organizationId === organization.id)
|
||||||
.map((c) => c.id);
|
.map((c) => c.id);
|
||||||
const deleteRequest = new CollectionBulkDeleteRequest(orgCollections, organization.id);
|
deletePromises.push(
|
||||||
deletePromises.push(this.apiService.deleteManyCollections(deleteRequest));
|
this.apiService.deleteManyCollections(this.organization.id, orgCollections)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return await Promise.all(deletePromises);
|
return await Promise.all(deletePromises);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||||
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
|
|
||||||
@ -22,6 +24,8 @@ export class VaultHeaderComponent {
|
|||||||
protected All = All;
|
protected All = All;
|
||||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||||
|
|
||||||
|
private flexibleCollectionsEnabled: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boolean to determine the loading state of the header.
|
* Boolean to determine the loading state of the header.
|
||||||
* Shows a loading spinner if set to true
|
* Shows a loading spinner if set to true
|
||||||
@ -55,7 +59,13 @@ export class VaultHeaderComponent {
|
|||||||
/** Emits an event when the delete collection button is clicked in the header */
|
/** Emits an event when the delete collection button is clicked in the header */
|
||||||
@Output() onDeleteCollection = new EventEmitter<void>();
|
@Output() onDeleteCollection = new EventEmitter<void>();
|
||||||
|
|
||||||
constructor(private i18nService: I18nService) {}
|
constructor(private i18nService: I18nService, private configService: ConfigServiceAbstraction) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.FlexibleCollections
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id of the organization that is currently being filtered on.
|
* The id of the organization that is currently being filtered on.
|
||||||
@ -146,11 +156,12 @@ export class VaultHeaderComponent {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, check if we can edit the specified collection
|
// Otherwise, check if we can delete the specified collection
|
||||||
const organization = this.organizations.find(
|
const organization = this.organizations.find(
|
||||||
(o) => o.id === this.collection?.node.organizationId
|
(o) => o.id === this.collection?.node.organizationId
|
||||||
);
|
);
|
||||||
return this.collection.node.canDelete(organization);
|
|
||||||
|
return this.collection.node.canDelete(organization, this.flexibleCollectionsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCollection() {
|
deleteCollection() {
|
||||||
|
@ -45,7 +45,9 @@
|
|||||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||||
[useEvents]="false"
|
[useEvents]="false"
|
||||||
[cloneableOrganizationCiphers]="false"
|
[cloneableOrganizationCiphers]="false"
|
||||||
|
[showAdminActions]="false"
|
||||||
(onEvent)="onVaultItemsEvent($event)"
|
(onEvent)="onVaultItemsEvent($event)"
|
||||||
|
[showBulkEditCollectionAccess]="showBulkCollectionAccess$ | async"
|
||||||
>
|
>
|
||||||
</app-vault-items>
|
</app-vault-items>
|
||||||
<div
|
<div
|
||||||
|
@ -39,6 +39,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
|||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { DEFAULT_PBKDF2_ITERATIONS, EventType, KdfType } from "@bitwarden/common/enums";
|
import { DEFAULT_PBKDF2_ITERATIONS, EventType, KdfType } from "@bitwarden/common/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
||||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
@ -143,6 +144,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
protected selectedCollection: TreeNode<CollectionView> | undefined;
|
||||||
protected canCreateCollections = false;
|
protected canCreateCollections = false;
|
||||||
protected currentSearchText$: Observable<string>;
|
protected currentSearchText$: Observable<string>;
|
||||||
|
protected showBulkCollectionAccess$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.BulkCollectionAccess,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
private searchText$ = new Subject<string>();
|
private searchText$ = new Subject<string>();
|
||||||
private refresh$ = new BehaviorSubject<void>(null);
|
private refresh$ = new BehaviorSubject<void>(null);
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
|
<bit-dialog [loading]="loading" dialogSize="large">
|
||||||
|
<span bitDialogTitle>
|
||||||
|
{{ "assignCollectionAccess" | i18n }}
|
||||||
|
<span class="tw-text-sm tw-normal-case tw-text-muted">
|
||||||
|
{{ numCollections }} {{ (numCollections == 1 ? "collection" : "collections") | i18n }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div bitDialogContent>
|
||||||
|
<bit-access-selector
|
||||||
|
*ngIf="organization?.useGroups"
|
||||||
|
[permissionMode]="PermissionMode.Edit"
|
||||||
|
formControlName="access"
|
||||||
|
[items]="accessItems"
|
||||||
|
[columnHeader]="'groupAndMemberColumnHeader' | i18n"
|
||||||
|
[selectorLabelText]="'selectGroupsAndMembers' | i18n"
|
||||||
|
[selectorHelpText]="'userPermissionOverrideHelper' | i18n"
|
||||||
|
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
|
||||||
|
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||||
|
></bit-access-selector>
|
||||||
|
<bit-access-selector
|
||||||
|
*ngIf="!organization?.useGroups"
|
||||||
|
[permissionMode]="PermissionMode.Edit"
|
||||||
|
formControlName="access"
|
||||||
|
[items]="accessItems"
|
||||||
|
[columnHeader]="'memberColumnHeader' | i18n"
|
||||||
|
[selectorLabelText]="'selectMembers' | i18n"
|
||||||
|
[emptySelectionText]="'noMembersAdded' | i18n"
|
||||||
|
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
|
||||||
|
></bit-access-selector>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container bitDialogFooter>
|
||||||
|
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||||
|
{{ "cancel" | i18n }}
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</bit-dialog>
|
||||||
|
</form>
|
@ -0,0 +1,137 @@
|
|||||||
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { Component, Inject, OnDestroy } from "@angular/core";
|
||||||
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { GroupService, GroupView } from "../../../admin-console/organizations/core";
|
||||||
|
import {
|
||||||
|
AccessItemType,
|
||||||
|
AccessItemValue,
|
||||||
|
AccessItemView,
|
||||||
|
AccessSelectorModule,
|
||||||
|
convertToSelectionView,
|
||||||
|
mapGroupToAccessItemView,
|
||||||
|
mapUserToAccessItemView,
|
||||||
|
PermissionMode,
|
||||||
|
} from "../../../admin-console/organizations/shared/components/access-selector";
|
||||||
|
import { SharedModule } from "../../../shared";
|
||||||
|
import { CollectionAdminService } from "../../core/collection-admin.service";
|
||||||
|
|
||||||
|
export interface BulkCollectionsDialogParams {
|
||||||
|
organizationId: string;
|
||||||
|
collections: CollectionView[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BulkCollectionsDialogResult {
|
||||||
|
Saved = "saved",
|
||||||
|
Canceled = "canceled",
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
imports: [SharedModule, AccessSelectorModule],
|
||||||
|
selector: "app-bulk-collections-dialog",
|
||||||
|
templateUrl: "bulk-collections-dialog.component.html",
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class BulkCollectionsDialogComponent implements OnDestroy {
|
||||||
|
protected flexibleCollectionsEnabled$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.FlexibleCollections,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
protected readonly PermissionMode = PermissionMode;
|
||||||
|
|
||||||
|
protected formGroup = this.formBuilder.group({
|
||||||
|
access: [[] as AccessItemValue[]],
|
||||||
|
});
|
||||||
|
protected loading = true;
|
||||||
|
protected organization: Organization;
|
||||||
|
protected accessItems: AccessItemView[] = [];
|
||||||
|
protected numCollections: number;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) private params: BulkCollectionsDialogParams,
|
||||||
|
private dialogRef: DialogRef<BulkCollectionsDialogResult>,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private organizationService: OrganizationService,
|
||||||
|
private groupService: GroupService,
|
||||||
|
private organizationUserService: OrganizationUserService,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private collectionAdminService: CollectionAdminService,
|
||||||
|
private configService: ConfigServiceAbstraction
|
||||||
|
) {
|
||||||
|
this.numCollections = this.params.collections.length;
|
||||||
|
const organization$ = this.organizationService.get$(this.params.organizationId);
|
||||||
|
const groups$ = organization$.pipe(
|
||||||
|
switchMap((organization) => {
|
||||||
|
if (!organization.useGroups) {
|
||||||
|
return of([] as GroupView[]);
|
||||||
|
}
|
||||||
|
return this.groupService.getAll(organization.id);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
organization$,
|
||||||
|
groups$,
|
||||||
|
this.organizationUserService.getAllUsers(this.params.organizationId),
|
||||||
|
])
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe(([organization, groups, users]) => {
|
||||||
|
this.organization = organization;
|
||||||
|
|
||||||
|
this.accessItems = [].concat(
|
||||||
|
groups.map(mapGroupToAccessItemView),
|
||||||
|
users.data.map(mapUserToAccessItemView)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
const users = this.formGroup.controls.access.value
|
||||||
|
.filter((v) => v.type === AccessItemType.Member)
|
||||||
|
.map(convertToSelectionView);
|
||||||
|
|
||||||
|
const groups = this.formGroup.controls.access.value
|
||||||
|
.filter((v) => v.type === AccessItemType.Group)
|
||||||
|
.map(convertToSelectionView);
|
||||||
|
|
||||||
|
await this.collectionAdminService.bulkAssignAccess(
|
||||||
|
this.organization.id,
|
||||||
|
this.params.collections.map((c) => c.id),
|
||||||
|
users,
|
||||||
|
groups
|
||||||
|
);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("editedCollections"));
|
||||||
|
|
||||||
|
this.dialogRef.close(BulkCollectionsDialogResult.Saved);
|
||||||
|
};
|
||||||
|
|
||||||
|
static open(dialogService: DialogService, config: DialogConfig<BulkCollectionsDialogParams>) {
|
||||||
|
return dialogService.open<BulkCollectionsDialogResult, BulkCollectionsDialogParams>(
|
||||||
|
BulkCollectionsDialogComponent,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./bulk-collections-dialog.component";
|
@ -5,7 +5,9 @@ import { firstValueFrom } from "rxjs";
|
|||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { ProductType } from "@bitwarden/common/enums";
|
import { ProductType } from "@bitwarden/common/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||||
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
||||||
|
|
||||||
@ -56,14 +58,23 @@ export class VaultHeaderComponent {
|
|||||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||||
protected organizations$ = this.organizationService.organizations$;
|
protected organizations$ = this.organizationService.organizations$;
|
||||||
|
|
||||||
|
private flexibleCollectionsEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private collectionAdminService: CollectionAdminService,
|
private collectionAdminService: CollectionAdminService,
|
||||||
private router: Router
|
private router: Router,
|
||||||
|
private configService: ConfigServiceAbstraction
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.flexibleCollectionsEnabled = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.FlexibleCollections
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
if (this.collection !== undefined) {
|
if (this.collection !== undefined) {
|
||||||
return this.collection.node.name;
|
return this.collection.node.name;
|
||||||
@ -171,7 +182,7 @@ export class VaultHeaderComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, check if we can delete the specified collection
|
// Otherwise, check if we can delete the specified collection
|
||||||
return this.collection.node.canDelete(this.organization);
|
return this.collection.node.canDelete(this.organization, this.flexibleCollectionsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCollection() {
|
deleteCollection() {
|
||||||
|
@ -52,7 +52,9 @@
|
|||||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||||
[useEvents]="organization?.useEvents"
|
[useEvents]="organization?.useEvents"
|
||||||
[cloneableOrganizationCiphers]="true"
|
[cloneableOrganizationCiphers]="true"
|
||||||
|
[showAdminActions]="true"
|
||||||
(onEvent)="onVaultItemsEvent($event)"
|
(onEvent)="onVaultItemsEvent($event)"
|
||||||
|
[showBulkEditCollectionAccess]="showBulkEditCollectionAccess$ | async"
|
||||||
>
|
>
|
||||||
</app-vault-items>
|
</app-vault-items>
|
||||||
<div
|
<div
|
||||||
|
@ -38,9 +38,11 @@ import { TotpService } from "@bitwarden/common/abstractions/totp.service";
|
|||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
import { ServiceUtils } from "@bitwarden/common/misc/serviceUtils";
|
||||||
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
@ -82,6 +84,10 @@ import { getNestedCollectionTree } from "../utils/collection-utils";
|
|||||||
|
|
||||||
import { AddEditComponent } from "./add-edit.component";
|
import { AddEditComponent } from "./add-edit.component";
|
||||||
import { AttachmentsComponent } from "./attachments.component";
|
import { AttachmentsComponent } from "./attachments.component";
|
||||||
|
import {
|
||||||
|
BulkCollectionsDialogComponent,
|
||||||
|
BulkCollectionsDialogResult,
|
||||||
|
} from "./bulk-collections-dialog";
|
||||||
import { CollectionsComponent } from "./collections.component";
|
import { CollectionsComponent } from "./collections.component";
|
||||||
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
|
||||||
|
|
||||||
@ -122,6 +128,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
protected isEmpty: boolean;
|
protected isEmpty: boolean;
|
||||||
protected showMissingCollectionPermissionMessage: boolean;
|
protected showMissingCollectionPermissionMessage: boolean;
|
||||||
protected currentSearchText$: Observable<string>;
|
protected currentSearchText$: Observable<string>;
|
||||||
|
protected showBulkEditCollectionAccess$ = this.configService.getFeatureFlag$(
|
||||||
|
FeatureFlag.BulkCollectionAccess,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
private searchText$ = new Subject<string>();
|
private searchText$ = new Subject<string>();
|
||||||
private refresh$ = new BehaviorSubject<void>(null);
|
private refresh$ = new BehaviorSubject<void>(null);
|
||||||
@ -152,7 +162,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private eventCollectionService: EventCollectionService,
|
private eventCollectionService: EventCollectionService,
|
||||||
private totpService: TotpService,
|
private totpService: TotpService,
|
||||||
private apiService: ApiService
|
private apiService: ApiService,
|
||||||
|
protected configService: ConfigServiceAbstraction
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -499,6 +510,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
await this.editCollection(event.item, CollectionDialogTabType.Info);
|
await this.editCollection(event.item, CollectionDialogTabType.Info);
|
||||||
} else if (event.type === "viewCollectionAccess") {
|
} else if (event.type === "viewCollectionAccess") {
|
||||||
await this.editCollection(event.item, CollectionDialogTabType.Access);
|
await this.editCollection(event.item, CollectionDialogTabType.Access);
|
||||||
|
} else if (event.type === "bulkEditCollectionAccess") {
|
||||||
|
await this.bulkEditCollectionAccess(event.items);
|
||||||
} else if (event.type === "viewEvents") {
|
} else if (event.type === "viewEvents") {
|
||||||
await this.viewEvents(event.item);
|
await this.viewEvents(event.item);
|
||||||
}
|
}
|
||||||
@ -890,6 +903,29 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async bulkEditCollectionAccess(collections: CollectionView[]): Promise<void> {
|
||||||
|
if (collections.length === 0) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
this.i18nService.t("errorOccurred"),
|
||||||
|
this.i18nService.t("nothingSelected")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialog = BulkCollectionsDialogComponent.open(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
collections,
|
||||||
|
organizationId: this.organization?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await lastValueFrom(dialog.closed);
|
||||||
|
if (result === BulkCollectionsDialogResult.Saved) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async viewEvents(cipher: CipherView) {
|
async viewEvents(cipher: CipherView) {
|
||||||
await openEntityEventsDialog(this.dialogService, {
|
await openEntityEventsDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
|
@ -1545,6 +1545,9 @@
|
|||||||
"manage": {
|
"manage": {
|
||||||
"message": "Manage"
|
"message": "Manage"
|
||||||
},
|
},
|
||||||
|
"canManage": {
|
||||||
|
"message": "Can manage"
|
||||||
|
},
|
||||||
"disable": {
|
"disable": {
|
||||||
"message": "Turn off"
|
"message": "Turn off"
|
||||||
},
|
},
|
||||||
@ -7218,6 +7221,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"collectionManagement": {
|
||||||
|
"message": "Collection management"
|
||||||
|
},
|
||||||
|
"collectionManagementDesc": {
|
||||||
|
"message": "Manage the collection behavior for the organization"
|
||||||
|
},
|
||||||
|
"limitCollectionCreationDeletionDesc": {
|
||||||
|
"message": "Limit collection creation and deletion to owners and admins"
|
||||||
|
},
|
||||||
|
"collectionManagementUpdated": {
|
||||||
|
"message": "Collection management behavior saved"
|
||||||
|
},
|
||||||
"passwordManagerPlanPrice": {
|
"passwordManagerPlanPrice": {
|
||||||
"message": "Password Manager plan price"
|
"message": "Password Manager plan price"
|
||||||
},
|
},
|
||||||
@ -7264,6 +7279,12 @@
|
|||||||
"beta": {
|
"beta": {
|
||||||
"message": "Beta"
|
"message": "Beta"
|
||||||
},
|
},
|
||||||
|
"assignCollectionAccess": {
|
||||||
|
"message": "Assign collection access"
|
||||||
|
},
|
||||||
|
"editedCollections": {
|
||||||
|
"message": "Edited collections"
|
||||||
|
},
|
||||||
"baseUrl": {
|
"baseUrl": {
|
||||||
"message": "Server URL"
|
"message": "Server URL"
|
||||||
},
|
},
|
||||||
@ -7273,6 +7294,15 @@
|
|||||||
"alreadyHaveAccount": {
|
"alreadyHaveAccount": {
|
||||||
"message": "Already have an account?"
|
"message": "Already have an account?"
|
||||||
},
|
},
|
||||||
|
"customBillingStart": {
|
||||||
|
"message": "Custom billing is not reflected. Visit the "
|
||||||
|
},
|
||||||
|
"customBillingEnd": {
|
||||||
|
"message": " page for latest invoicing."
|
||||||
|
},
|
||||||
|
"managePermissionRequired": {
|
||||||
|
"message": "At least one member or group must have can manage permission."
|
||||||
|
},
|
||||||
"typePasskey": {
|
"typePasskey": {
|
||||||
"message": "Passkey"
|
"message": "Passkey"
|
||||||
},
|
},
|
||||||
|
@ -99,7 +99,6 @@ import { PlanResponse } from "../billing/models/response/plan.response";
|
|||||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||||
import { TaxRateResponse } from "../billing/models/response/tax-rate.response";
|
import { TaxRateResponse } from "../billing/models/response/tax-rate.response";
|
||||||
import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request";
|
|
||||||
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||||
import { EventRequest } from "../models/request/event.request";
|
import { EventRequest } from "../models/request/event.request";
|
||||||
import { IapCheckRequest } from "../models/request/iap-check.request";
|
import { IapCheckRequest } from "../models/request/iap-check.request";
|
||||||
@ -301,7 +300,7 @@ export abstract class ApiService {
|
|||||||
request: CollectionRequest
|
request: CollectionRequest
|
||||||
) => Promise<CollectionResponse>;
|
) => Promise<CollectionResponse>;
|
||||||
deleteCollection: (organizationId: string, id: string) => Promise<any>;
|
deleteCollection: (organizationId: string, id: string) => Promise<any>;
|
||||||
deleteManyCollections: (request: CollectionBulkDeleteRequest) => Promise<any>;
|
deleteManyCollections: (organizationId: string, collectionIds: string[]) => Promise<any>;
|
||||||
deleteCollectionUser: (
|
deleteCollectionUser: (
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -18,6 +18,7 @@ import { StorageRequest } from "../../../models/request/storage.request";
|
|||||||
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
|
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
|
||||||
import { ListResponse } from "../../../models/response/list.response";
|
import { ListResponse } from "../../../models/response/list.response";
|
||||||
import { OrganizationApiKeyType } from "../../enums";
|
import { OrganizationApiKeyType } from "../../enums";
|
||||||
|
import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request";
|
||||||
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
||||||
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
||||||
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
||||||
@ -73,4 +74,8 @@ export class OrganizationApiServiceAbstraction {
|
|||||||
id: string,
|
id: string,
|
||||||
request: SecretsManagerSubscribeRequest
|
request: SecretsManagerSubscribeRequest
|
||||||
) => Promise<ProfileOrganizationResponse>;
|
) => Promise<ProfileOrganizationResponse>;
|
||||||
|
updateCollectionManagement: (
|
||||||
|
id: string,
|
||||||
|
request: OrganizationCollectionManagementUpdateRequest
|
||||||
|
) => Promise<OrganizationResponse>;
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ export class OrganizationData {
|
|||||||
familySponsorshipValidUntil?: Date;
|
familySponsorshipValidUntil?: Date;
|
||||||
familySponsorshipToDelete?: boolean;
|
familySponsorshipToDelete?: boolean;
|
||||||
accessSecretsManager: boolean;
|
accessSecretsManager: boolean;
|
||||||
|
limitCollectionCreationDeletion: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
response: ProfileOrganizationResponse,
|
response: ProfileOrganizationResponse,
|
||||||
@ -100,6 +101,7 @@ export class OrganizationData {
|
|||||||
this.familySponsorshipValidUntil = response.familySponsorshipValidUntil;
|
this.familySponsorshipValidUntil = response.familySponsorshipValidUntil;
|
||||||
this.familySponsorshipToDelete = response.familySponsorshipToDelete;
|
this.familySponsorshipToDelete = response.familySponsorshipToDelete;
|
||||||
this.accessSecretsManager = response.accessSecretsManager;
|
this.accessSecretsManager = response.accessSecretsManager;
|
||||||
|
this.limitCollectionCreationDeletion = response.limitCollectionCreationDeletion;
|
||||||
|
|
||||||
this.isMember = options.isMember;
|
this.isMember = options.isMember;
|
||||||
this.isProviderUser = options.isProviderUser;
|
this.isProviderUser = options.isProviderUser;
|
||||||
|
@ -64,6 +64,10 @@ export class Organization {
|
|||||||
familySponsorshipValidUntil?: Date;
|
familySponsorshipValidUntil?: Date;
|
||||||
familySponsorshipToDelete?: boolean;
|
familySponsorshipToDelete?: boolean;
|
||||||
accessSecretsManager: boolean;
|
accessSecretsManager: boolean;
|
||||||
|
/**
|
||||||
|
* Refers to the ability for an organization to limit collection creation and deletion to owners and admins only
|
||||||
|
*/
|
||||||
|
limitCollectionCreationDeletion: boolean;
|
||||||
|
|
||||||
constructor(obj?: OrganizationData) {
|
constructor(obj?: OrganizationData) {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
@ -115,6 +119,7 @@ export class Organization {
|
|||||||
this.familySponsorshipValidUntil = obj.familySponsorshipValidUntil;
|
this.familySponsorshipValidUntil = obj.familySponsorshipValidUntil;
|
||||||
this.familySponsorshipToDelete = obj.familySponsorshipToDelete;
|
this.familySponsorshipToDelete = obj.familySponsorshipToDelete;
|
||||||
this.accessSecretsManager = obj.accessSecretsManager;
|
this.accessSecretsManager = obj.accessSecretsManager;
|
||||||
|
this.limitCollectionCreationDeletion = obj.limitCollectionCreationDeletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canAccess() {
|
get canAccess() {
|
||||||
@ -158,7 +163,9 @@ export class Organization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get canCreateNewCollections() {
|
get canCreateNewCollections() {
|
||||||
return this.isManager || this.permissions.createNewCollections;
|
return (
|
||||||
|
!this.limitCollectionCreationDeletion || this.isAdmin || this.permissions.createNewCollections
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get canEditAnyCollection() {
|
get canEditAnyCollection() {
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export class OrganizationCollectionManagementUpdateRequest {
|
||||||
|
limitCreateDeleteOwnerAdmin: boolean;
|
||||||
|
}
|
@ -2,10 +2,12 @@ export class SelectionReadOnlyRequest {
|
|||||||
id: string;
|
id: string;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
hidePasswords: boolean;
|
hidePasswords: boolean;
|
||||||
|
manage: boolean;
|
||||||
|
|
||||||
constructor(id: string, readOnly: boolean, hidePasswords: boolean) {
|
constructor(id: string, readOnly: boolean, hidePasswords: boolean, manage: boolean) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.readOnly = readOnly;
|
this.readOnly = readOnly;
|
||||||
this.hidePasswords = hidePasswords;
|
this.hidePasswords = hidePasswords;
|
||||||
|
this.manage = manage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ export class OrganizationResponse extends BaseResponse {
|
|||||||
smServiceAccounts?: number;
|
smServiceAccounts?: number;
|
||||||
maxAutoscaleSmSeats?: number;
|
maxAutoscaleSmSeats?: number;
|
||||||
maxAutoscaleSmServiceAccounts?: number;
|
maxAutoscaleSmServiceAccounts?: number;
|
||||||
|
limitCollectionCreationDeletion: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@ -67,5 +68,8 @@ export class OrganizationResponse extends BaseResponse {
|
|||||||
this.smServiceAccounts = this.getResponseProperty("SmServiceAccounts");
|
this.smServiceAccounts = this.getResponseProperty("SmServiceAccounts");
|
||||||
this.maxAutoscaleSmSeats = this.getResponseProperty("MaxAutoscaleSmSeats");
|
this.maxAutoscaleSmSeats = this.getResponseProperty("MaxAutoscaleSmSeats");
|
||||||
this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts");
|
this.maxAutoscaleSmServiceAccounts = this.getResponseProperty("MaxAutoscaleSmServiceAccounts");
|
||||||
|
this.limitCollectionCreationDeletion = this.getResponseProperty(
|
||||||
|
"LimitCollectionCreationDeletion"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
|||||||
familySponsorshipValidUntil?: Date;
|
familySponsorshipValidUntil?: Date;
|
||||||
familySponsorshipToDelete?: boolean;
|
familySponsorshipToDelete?: boolean;
|
||||||
accessSecretsManager: boolean;
|
accessSecretsManager: boolean;
|
||||||
|
limitCollectionCreationDeletion: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@ -105,5 +106,8 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
|||||||
}
|
}
|
||||||
this.familySponsorshipToDelete = this.getResponseProperty("FamilySponsorshipToDelete");
|
this.familySponsorshipToDelete = this.getResponseProperty("FamilySponsorshipToDelete");
|
||||||
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
|
this.accessSecretsManager = this.getResponseProperty("AccessSecretsManager");
|
||||||
|
this.limitCollectionCreationDeletion = this.getResponseProperty(
|
||||||
|
"LimitCollectionCreationDeletion"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,13 @@ export class SelectionReadOnlyResponse extends BaseResponse {
|
|||||||
id: string;
|
id: string;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
hidePasswords: boolean;
|
hidePasswords: boolean;
|
||||||
|
manage: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
this.id = this.getResponseProperty("Id");
|
this.id = this.getResponseProperty("Id");
|
||||||
this.readOnly = this.getResponseProperty("ReadOnly");
|
this.readOnly = this.getResponseProperty("ReadOnly");
|
||||||
this.hidePasswords = this.getResponseProperty("HidePasswords");
|
this.hidePasswords = this.getResponseProperty("HidePasswords");
|
||||||
|
this.manage = this.getResponseProperty("Manage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import { ListResponse } from "../../../models/response/list.response";
|
|||||||
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationApiKeyType } from "../../enums";
|
import { OrganizationApiKeyType } from "../../enums";
|
||||||
|
import { OrganizationCollectionManagementUpdateRequest } from "../../models/request/organization-collection-management-update.request";
|
||||||
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
|
||||||
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
|
||||||
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";
|
||||||
@ -322,4 +323,20 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction
|
|||||||
);
|
);
|
||||||
return new ProfileOrganizationResponse(r);
|
return new ProfileOrganizationResponse(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateCollectionManagement(
|
||||||
|
id: string,
|
||||||
|
request: OrganizationCollectionManagementUpdateRequest
|
||||||
|
): Promise<OrganizationResponse> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"PUT",
|
||||||
|
"/organizations/" + id + "/collection-management",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const data = new OrganizationResponse(r);
|
||||||
|
await this.syncService.fullSync(true);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ export enum FeatureFlag {
|
|||||||
AutofillV2 = "autofill-v2",
|
AutofillV2 = "autofill-v2",
|
||||||
BrowserFilelessImport = "browser-fileless-import",
|
BrowserFilelessImport = "browser-fileless-import",
|
||||||
ItemShare = "item-share",
|
ItemShare = "item-share",
|
||||||
|
FlexibleCollections = "flexible-collections",
|
||||||
|
BulkCollectionAccess = "bulk-collection-access",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
// Replace this with a type safe lookup of the feature flag values in PM-2282
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
export class CollectionBulkDeleteRequest {
|
export class CollectionBulkDeleteRequest {
|
||||||
ids: string[];
|
ids: string[];
|
||||||
organizationId: string;
|
|
||||||
|
|
||||||
constructor(ids: string[], organizationId?: string) {
|
constructor(ids: string[]) {
|
||||||
this.ids = ids == null ? [] : ids;
|
this.ids = ids == null ? [] : ids;
|
||||||
this.organizationId = organizationId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -834,11 +834,11 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteManyCollections(request: CollectionBulkDeleteRequest): Promise<any> {
|
deleteManyCollections(organizationId: string, collectionIds: string[]): Promise<any> {
|
||||||
return this.send(
|
return this.send(
|
||||||
"DELETE",
|
"DELETE",
|
||||||
"/organizations/" + request.organizationId + "/collections",
|
"/organizations/" + organizationId + "/collections",
|
||||||
request,
|
new CollectionBulkDeleteRequest(collectionIds),
|
||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,7 @@ export class CollectionData {
|
|||||||
name: string;
|
name: string;
|
||||||
externalId: string;
|
externalId: string;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
|
manage: boolean;
|
||||||
hidePasswords: boolean;
|
hidePasswords: boolean;
|
||||||
|
|
||||||
constructor(response: CollectionDetailsResponse) {
|
constructor(response: CollectionDetailsResponse) {
|
||||||
@ -14,6 +15,7 @@ export class CollectionData {
|
|||||||
this.name = response.name;
|
this.name = response.name;
|
||||||
this.externalId = response.externalId;
|
this.externalId = response.externalId;
|
||||||
this.readOnly = response.readOnly;
|
this.readOnly = response.readOnly;
|
||||||
|
this.manage = response.manage;
|
||||||
this.hidePasswords = response.hidePasswords;
|
this.hidePasswords = response.hidePasswords;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ describe("Collection", () => {
|
|||||||
name: "encName",
|
name: "encName",
|
||||||
externalId: "extId",
|
externalId: "extId",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
manage: true,
|
||||||
hidePasswords: true,
|
hidePasswords: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -28,6 +29,7 @@ describe("Collection", () => {
|
|||||||
name: null,
|
name: null,
|
||||||
organizationId: null,
|
organizationId: null,
|
||||||
readOnly: null,
|
readOnly: null,
|
||||||
|
manage: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -40,6 +42,7 @@ describe("Collection", () => {
|
|||||||
name: { encryptedString: "encName", encryptionType: 0 },
|
name: { encryptedString: "encName", encryptionType: 0 },
|
||||||
externalId: "extId",
|
externalId: "extId",
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
manage: true,
|
||||||
hidePasswords: true,
|
hidePasswords: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -52,6 +55,7 @@ describe("Collection", () => {
|
|||||||
collection.externalId = "extId";
|
collection.externalId = "extId";
|
||||||
collection.readOnly = false;
|
collection.readOnly = false;
|
||||||
collection.hidePasswords = false;
|
collection.hidePasswords = false;
|
||||||
|
collection.manage = true;
|
||||||
|
|
||||||
const view = await collection.decrypt();
|
const view = await collection.decrypt();
|
||||||
|
|
||||||
@ -62,6 +66,7 @@ describe("Collection", () => {
|
|||||||
name: "encName",
|
name: "encName",
|
||||||
organizationId: "orgId",
|
organizationId: "orgId",
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
manage: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,7 @@ export class Collection extends Domain {
|
|||||||
externalId: string;
|
externalId: string;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
hidePasswords: boolean;
|
hidePasswords: boolean;
|
||||||
|
manage: boolean;
|
||||||
|
|
||||||
constructor(obj?: CollectionData) {
|
constructor(obj?: CollectionData) {
|
||||||
super();
|
super();
|
||||||
@ -27,8 +28,9 @@ export class Collection extends Domain {
|
|||||||
externalId: null,
|
externalId: null,
|
||||||
readOnly: null,
|
readOnly: null,
|
||||||
hidePasswords: null,
|
hidePasswords: null,
|
||||||
|
manage: null,
|
||||||
},
|
},
|
||||||
["id", "organizationId", "externalId", "readOnly", "hidePasswords"]
|
["id", "organizationId", "externalId", "readOnly", "hidePasswords", "manage"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,11 +18,13 @@ export class CollectionResponse extends BaseResponse {
|
|||||||
|
|
||||||
export class CollectionDetailsResponse extends CollectionResponse {
|
export class CollectionDetailsResponse extends CollectionResponse {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
|
manage: boolean;
|
||||||
hidePasswords: boolean;
|
hidePasswords: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
this.readOnly = this.getResponseProperty("ReadOnly") || false;
|
this.readOnly = this.getResponseProperty("ReadOnly") || false;
|
||||||
|
this.manage = this.getResponseProperty("Manage") || false;
|
||||||
this.hidePasswords = this.getResponseProperty("HidePasswords") || false;
|
this.hidePasswords = this.getResponseProperty("HidePasswords") || false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
// readOnly applies to the items within a collection
|
// readOnly applies to the items within a collection
|
||||||
readOnly: boolean = null;
|
readOnly: boolean = null;
|
||||||
hidePasswords: boolean = null;
|
hidePasswords: boolean = null;
|
||||||
|
manage: boolean = null;
|
||||||
|
|
||||||
constructor(c?: Collection | CollectionAccessDetailsResponse) {
|
constructor(c?: Collection | CollectionAccessDetailsResponse) {
|
||||||
if (!c) {
|
if (!c) {
|
||||||
@ -26,6 +27,7 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
if (c instanceof Collection) {
|
if (c instanceof Collection) {
|
||||||
this.readOnly = c.readOnly;
|
this.readOnly = c.readOnly;
|
||||||
this.hidePasswords = c.hidePasswords;
|
this.hidePasswords = c.hidePasswords;
|
||||||
|
this.manage = c.manage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,12 +42,17 @@ export class CollectionView implements View, ITreeNodeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For deleting a collection, not the items within it.
|
// For deleting a collection, not the items within it.
|
||||||
canDelete(org: Organization): boolean {
|
canDelete(org: Organization, flexibleCollectionsEnabled: boolean): boolean {
|
||||||
if (org.id !== this.organizationId) {
|
if (org.id !== this.organizationId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Id of the organization provided does not match the org id of the collection."
|
"Id of the organization provided does not match the org id of the collection."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
|
|
||||||
|
if (flexibleCollectionsEnabled) {
|
||||||
|
return org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage);
|
||||||
|
} else {
|
||||||
|
return org?.canDeleteAnyCollection || org?.canDeleteAssignedCollections;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user