diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index b6175d80297a..66aac8dc4d4a 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -40,6 +40,7 @@ [columnHeader]="'member' | i18n" [selectorLabelText]="'selectMembers' | i18n" [emptySelectionText]="'noMembersAdded' | i18n" + [flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async" > @@ -60,6 +61,7 @@ [columnHeader]="'collection' | i18n" [selectorLabelText]="'selectCollections' | i18n" [emptySelectionText]="'noCollectionsAdded' | i18n" + [flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async" > diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index d2f4fb1d20da..00e7b3872dc6 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -5,7 +5,9 @@ import { catchError, combineLatest, from, map, of, Subject, switchMap, takeUntil import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationUserService } from "@bitwarden/common/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 { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -78,6 +80,11 @@ export const openGroupAddEditDialog = ( templateUrl: "group-add-edit.component.html", }) export class GroupAddEditComponent implements OnInit, OnDestroy { + protected flexibleCollectionsEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollections, + false + ); + protected PermissionMode = PermissionMode; protected ResultType = GroupAddEditDialogResultType; @@ -181,7 +188,8 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private logService: LogService, private formBuilder: FormBuilder, private changeDetectorRef: ChangeDetectorRef, - private dialogService: DialogService + private dialogService: DialogService, + private configService: ConfigServiceAbstraction ) { this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info; } diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 8c506837880e..34d407e7b23a 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -289,6 +289,7 @@

[columnHeader]="'groups' | i18n" [selectorLabelText]="'selectGroups' | i18n" [emptySelectionText]="'noGroupsAdded' | i18n" + [flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async" > @@ -321,6 +322,7 @@

[columnHeader]="'collection' | i18n" [selectorLabelText]="'selectCollections' | i18n" [emptySelectionText]="'noCollectionsAdded' | i18n" + [flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async" > diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index 1d1a156269b4..69efbd3fe766 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -11,6 +11,8 @@ import { } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; 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"; @@ -64,6 +66,11 @@ export enum MemberDialogResult { templateUrl: "member-dialog.component.html", }) export class MemberDialogComponent implements OnInit, OnDestroy { + protected flexibleCollectionsEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollections, + false + ); + loading = true; editMode = false; isRevoked = false; @@ -134,7 +141,8 @@ export class MemberDialogComponent implements OnInit, OnDestroy { private groupService: GroupService, private userService: UserAdminService, private organizationUserService: OrganizationUserService, - private dialogService: DialogService + private dialogService: DialogService, + private configService: ConfigServiceAbstraction ) {} async ngOnInit() { diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index 61b4a3dc8e6f..bbce9b2e51e5 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -53,7 +53,7 @@

{{ "apiKey" | i18n }}

diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index b7ecae82fca4..693e718231be 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -10,6 +10,8 @@ import { OrganizationCollectionManagementUpdateRequest } from "@bitwarden/common import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/models/request/organization-update.request"; import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -39,6 +41,10 @@ export class AccountComponent { canUseApi = false; org: OrganizationResponse; taxFormPromise: Promise; + showCollectionManagementSettings$ = this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollections, + false + ); // FormGroup validators taken from server Organization domain object protected formGroup = this.formBuilder.group({ @@ -78,7 +84,8 @@ export class AccountComponent { private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, - private formBuilder: FormBuilder + private formBuilder: FormBuilder, + private configService: ConfigServiceAbstraction ) {} async ngOnInit() { diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts index 018f568a420c..2b954063b2a4 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts @@ -121,8 +121,11 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On { perm: CollectionPermission.ViewExceptPass, labelId: "canViewExceptPass" }, { perm: CollectionPermission.Edit, labelId: "canEdit" }, { perm: CollectionPermission.EditExceptPass, labelId: "canEditExceptPass" }, - { perm: CollectionPermission.Manage, labelId: "canManage" }, ]; + private canManagePermissionListItem = { + perm: CollectionPermission.Manage, + labelId: "canManage", + }; protected initialPermission = CollectionPermission.View; disabled: boolean; @@ -193,6 +196,11 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On */ @Input() showGroupColumn: boolean; + /** + * Enable Flexible Collections changes (feature flag) + */ + @Input() flexibleCollectionsEnabled: boolean; + constructor( private readonly formBuilder: FormBuilder, private readonly i18nService: I18nService @@ -255,7 +263,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On this.pauseChangeNotification = false; } - ngOnInit() { + async ngOnInit() { // Watch the internal formArray for changes and propagate them this.selectionList.formArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => { if (!this.notifyOnChange || this.pauseChangeNotification) { @@ -269,6 +277,10 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On } this.notifyOnChange(v); }); + + if (this.flexibleCollectionsEnabled) { + this.permissionList.push(this.canManagePermissionListItem); + } } ngOnDestroy() { diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html index 4f8c04b2b04a..41e785a6eba5 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.html @@ -79,6 +79,7 @@ [selectorLabelText]="'selectGroupsAndMembers' | i18n" [selectorHelpText]="'userPermissionOverrideHelper' | i18n" [emptySelectionText]="'noMembersOrGroupsAdded' | i18n" + [flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async" > diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 8fd7b2a4f78e..23b2ec3a8178 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angula import { AbstractControl, FormBuilder, Validators } from "@angular/forms"; import { combineLatest, + firstValueFrom, from, map, Observable, @@ -16,6 +17,8 @@ import { import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -67,6 +70,11 @@ export enum CollectionDialogAction { templateUrl: "collection-dialog.component.html", }) export class CollectionDialogComponent implements OnInit, OnDestroy { + protected flexibleCollectionsEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.FlexibleCollections, + false + ); + private destroy$ = new Subject(); protected organizations$: Observable; @@ -82,7 +90,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { name: ["", [Validators.required, BitValidators.forbiddenCharacters(["/"])]], externalId: "", parent: undefined as string | undefined, - access: [[] as AccessItemValue[], [validateCanManagePermission]], + access: [[] as AccessItemValue[]], selectedOrg: "", }); protected PermissionMode = PermissionMode; @@ -98,7 +106,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private organizationUserService: OrganizationUserService, private dialogService: DialogService, - private changeDetectorRef: ChangeDetectorRef + private changeDetectorRef: ChangeDetectorRef, + private configService: ConfigServiceAbstraction ) { this.tabIndex = params.initialTab ?? CollectionDialogTabType.Info; } @@ -124,6 +133,10 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { this.formGroup.patchValue({ selectedOrg: this.params.organizationId }); await this.loadOrg(this.params.organizationId, this.params.collectionIds); } + + if (await firstValueFrom(this.flexibleCollectionsEnabled$)) { + this.formGroup.controls.access.addValidators(validateCanManagePermission); + } } async loadOrg(orgId: string, collectionIds: string[]) { @@ -147,67 +160,72 @@ export class CollectionDialogComponent implements OnInit, OnDestroy { : of(null), groups: groups$, users: this.organizationUserService.getAllUsers(orgId), + flexibleCollections: this.flexibleCollectionsEnabled$, }) .pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$)) - .subscribe(({ organization, collections, collectionDetails, groups, users }) => { - this.organization = organization; - this.accessItems = [].concat( - groups.map(mapGroupToAccessItemView), - users.data.map(mapUserToAccessItemView) - ); - - // Force change detection to update the access selector's items - this.changeDetectorRef.detectChanges(); - - if (collectionIds) { - collections = collections.filter((c) => collectionIds.includes(c.id)); - } - - if (this.params.collectionId) { - this.collection = collections.find((c) => c.id === this.collectionId); - this.nestOptions = collections.filter((c) => c.id !== this.collectionId); - - if (!this.collection) { - throw new Error("Could not find collection to edit."); + .subscribe( + ({ organization, collections, collectionDetails, groups, users, flexibleCollections }) => { + this.organization = organization; + this.accessItems = [].concat( + groups.map(mapGroupToAccessItemView), + users.data.map(mapUserToAccessItemView) + ); + + // Force change detection to update the access selector's items + this.changeDetectorRef.detectChanges(); + + if (collectionIds) { + collections = collections.filter((c) => collectionIds.includes(c.id)); } - const { name, parent } = parseName(this.collection); - if (parent !== undefined && !this.nestOptions.find((c) => c.name === parent)) { - this.deletedParentName = parent; + if (this.params.collectionId) { + this.collection = collections.find((c) => c.id === this.collectionId); + 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.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: CollectionPermission.Manage, - }, - ] - : []; - - this.formGroup.patchValue({ - parent: parent?.name ?? undefined, - access: initialSelection, - }); + this.loading = false; } - - this.loading = false; - }); + ); } protected get collectionId() { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index 7176e2f18e9d..371737fd43c3 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -35,7 +35,7 @@ {{ "moveSelected" | i18n }}