Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AC-1713] [Flexible collections] Add feature flags to clients #6486

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
[columnHeader]="'member' | i18n"
[selectorLabelText]="'selectMembers' | i18n"
[emptySelectionText]="'noMembersAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector>
</bit-tab>

Expand All @@ -60,6 +61,7 @@
[columnHeader]="'collection' | i18n"
[selectorLabelText]="'selectCollections' | i18n"
[emptySelectionText]="'noCollectionsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector>
</ng-container>
</bit-tab>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ <h3 class="mt-4">
[columnHeader]="'groups' | i18n"
[selectorLabelText]="'selectGroups' | i18n"
[emptySelectionText]="'noGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector>
</bit-tab>
<bit-tab [label]="'collections' | i18n">
Expand Down Expand Up @@ -321,6 +322,7 @@ <h3 class="mt-4">
[columnHeader]="'collection' | i18n"
[selectorLabelText]="'selectCollections' | i18n"
[emptySelectionText]="'noCollectionsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector
></bit-tab>
</bit-tab-group>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ <h1 bitTypography="h1" class="tw-mt-16 tw-pb-2.5">{{ "apiKey" | i18n }}</h1>
</button>
</ng-container>
<form
*ngIf="org && !loading"
*ngIf="org && !loading && (showCollectionManagementSettings$ | async)"
[bitSubmit]="submitCollectionManagement"
[formGroup]="collectionManagementFormGroup"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -39,6 +41,10 @@ export class AccountComponent {
canUseApi = false;
org: OrganizationResponse;
taxFormPromise: Promise<unknown>;
showCollectionManagementSettings$ = this.configService.getFeatureFlag$(
FeatureFlag.FlexibleCollections,
false
);

// FormGroup validators taken from server Organization domain object
protected formGroup = this.formBuilder.group({
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -269,6 +277,10 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On
}
this.notifyOnChange(v);
});

if (this.flexibleCollectionsEnabled) {
this.permissionList.push(this.canManagePermissionListItem);
}
}

ngOnDestroy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
[selectorLabelText]="'selectGroupsAndMembers' | i18n"
[selectorHelpText]="'userPermissionOverrideHelper' | i18n"
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector>
<bit-access-selector
*ngIf="!organization.useGroups"
Expand All @@ -88,6 +89,7 @@
[columnHeader]="'memberColumnHeader' | i18n"
[selectorLabelText]="'selectMembers' | i18n"
[emptySelectionText]="'noMembersAdded' | i18n"
[flexibleCollectionsEnabled]="flexibleCollectionsEnabled$ | async"
></bit-access-selector>
</bit-tab>
</bit-tab-group>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand Down Expand Up @@ -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<void>();
protected organizations$: Observable<Organization[]>;

Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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[]) {
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
{{ "moveSelected" | i18n }}
</button>
<button
*ngIf="showAdminActions"
*ngIf="showAdminActions && showBulkEditCollectionAccess"
type="button"
bitMenuItem
(click)="bulkEditCollectionAccess()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class VaultItemsComponent {
@Input() allOrganizations: Organization[] = [];
@Input() allCollections: CollectionView[] = [];
@Input() allGroups: GroupView[] = [];
@Input() showBulkEditCollectionAccess = false;

private _ciphers?: CipherView[] = [];
@Input() get ciphers(): CipherView[] {
Expand Down
Loading