[SM-919] Migrate Project people tab to access-policy-selector (#6431)
* Add access-policy-selector * Update to access-policy service and models * Add access-policy service tests * Use new selector in project-people * Fix access removal dialog bug (#6653)
This commit is contained in:
parent
a7a3783501
commit
ec205d4224
|
@ -7386,5 +7386,8 @@
|
||||||
"seeDetailedInstructions": {
|
"seeDetailedInstructions": {
|
||||||
"message": "See detailed instructions on our help site at",
|
"message": "See detailed instructions on our help site at",
|
||||||
"description": "This is followed a by a hyperlink to the help website."
|
"description": "This is followed a by a hyperlink to the help website."
|
||||||
|
},
|
||||||
|
"projectAccessUpdated": {
|
||||||
|
"message": "Project access updated"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export class UserProjectAccessPolicyView extends BaseAccessPolicyView {
|
||||||
organizationUserName: string;
|
organizationUserName: string;
|
||||||
grantedProjectId: string;
|
grantedProjectId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currentUser: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView {
|
export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView {
|
||||||
|
@ -47,6 +48,11 @@ export class ProjectAccessPoliciesView {
|
||||||
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[];
|
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ProjectPeopleAccessPoliciesView {
|
||||||
|
userAccessPolicies: UserProjectAccessPolicyView[];
|
||||||
|
groupAccessPolicies: GroupProjectAccessPolicyView[];
|
||||||
|
}
|
||||||
|
|
||||||
export class ServiceAccountAccessPoliciesView {
|
export class ServiceAccountAccessPoliciesView {
|
||||||
userAccessPolicies: UserServiceAccountAccessPolicyView[];
|
userAccessPolicies: UserServiceAccountAccessPolicyView[];
|
||||||
groupAccessPolicies: GroupServiceAccountAccessPolicyView[];
|
groupAccessPolicies: GroupServiceAccountAccessPolicyView[];
|
||||||
|
|
|
@ -3,4 +3,6 @@ export class PotentialGranteeView {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
currentUserInGroup: boolean;
|
||||||
|
currentUser: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
<div class="tw-w-2/5">
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
<p class="tw-mt-8">
|
<div class="tw-w-2/5">
|
||||||
{{ "projectPeopleDescription" | i18n }}
|
<p class="tw-mt-8" *ngIf="!loading">
|
||||||
</p>
|
{{ "projectPeopleDescription" | i18n }}
|
||||||
<sm-access-selector
|
</p>
|
||||||
[rows]="rows$ | async"
|
<sm-access-policy-selector
|
||||||
granteeType="people"
|
[loading]="loading"
|
||||||
[label]="'people' | i18n"
|
formControlName="accessPolicies"
|
||||||
[hint]="'projectPeopleSelectHint' | i18n"
|
[addButtonMode]="true"
|
||||||
[columnTitle]="'name' | i18n"
|
[items]="potentialGrantees"
|
||||||
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n"
|
[label]="'people' | i18n"
|
||||||
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)"
|
[hint]="'projectPeopleSelectHint' | i18n"
|
||||||
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)"
|
[columnTitle]="'name' | i18n"
|
||||||
(onUpdateAccessPolicy)="handleUpdateAccessPolicy($event)"
|
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n"
|
||||||
>
|
>
|
||||||
</sm-access-selector>
|
</sm-access-policy-selector>
|
||||||
</div>
|
<button bitButton buttonType="primary" bitFormButton type="submit" class="tw-mt-7">
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
|
@ -1,151 +1,75 @@
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { FormControl, FormGroup } from "@angular/forms";
|
||||||
import { map, Observable, share, startWith, Subject, switchMap, takeUntil } from "rxjs";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
import { combineLatest, Subject, switchMap, takeUntil, catchError, EMPTY } from "rxjs";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { DialogService, SelectItemView } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service";
|
||||||
import {
|
import {
|
||||||
GroupProjectAccessPolicyView,
|
ApItemValueType,
|
||||||
ProjectAccessPoliciesView,
|
convertToProjectPeopleAccessPoliciesView,
|
||||||
UserProjectAccessPolicyView,
|
} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
|
||||||
} from "../../models/view/access-policy.view";
|
import {
|
||||||
|
ApItemViewType,
|
||||||
|
convertPotentialGranteesToApItemViewType,
|
||||||
|
convertToAccessPolicyItemViews,
|
||||||
|
} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
|
||||||
|
import { ApItemEnum } from "../../shared/access-policies/access-policy-selector/models/enums/ap-item.enum";
|
||||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||||
import {
|
|
||||||
AccessSelectorComponent,
|
|
||||||
AccessSelectorRowView,
|
|
||||||
} from "../../shared/access-policies/access-selector.component";
|
|
||||||
import {
|
|
||||||
AccessRemovalDetails,
|
|
||||||
AccessRemovalDialogComponent,
|
|
||||||
} from "../../shared/access-policies/dialogs/access-removal-dialog.component";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "sm-project-people",
|
selector: "sm-project-people",
|
||||||
templateUrl: "./project-people.component.html",
|
templateUrl: "./project-people.component.html",
|
||||||
})
|
})
|
||||||
export class ProjectPeopleComponent implements OnInit, OnDestroy {
|
export class ProjectPeopleComponent implements OnInit, OnDestroy {
|
||||||
|
private currentAccessPolicies: ApItemViewType[];
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
private projectId: string;
|
private projectId: string;
|
||||||
private rows: AccessSelectorRowView[];
|
|
||||||
|
|
||||||
protected rows$: Observable<AccessSelectorRowView[]> =
|
private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
|
||||||
this.accessPolicyService.projectAccessPolicyChanges$.pipe(
|
switchMap(([params]) =>
|
||||||
startWith(null),
|
this.accessPolicyService.getProjectPeopleAccessPolicies(params.projectId).then((policies) => {
|
||||||
switchMap(() =>
|
return convertToAccessPolicyItemViews(policies);
|
||||||
this.accessPolicyService.getProjectAccessPolicies(this.organizationId, this.projectId)
|
})
|
||||||
),
|
),
|
||||||
map((policies) => {
|
catchError(() => {
|
||||||
const rows: AccessSelectorRowView[] = [];
|
this.router.navigate(["/sm", this.organizationId, "projects"]);
|
||||||
policies.userAccessPolicies.forEach((policy) => {
|
return EMPTY;
|
||||||
rows.push({
|
})
|
||||||
type: "user",
|
);
|
||||||
name: policy.organizationUserName,
|
|
||||||
id: policy.organizationUserId,
|
|
||||||
accessPolicyId: policy.id,
|
|
||||||
read: policy.read,
|
|
||||||
write: policy.write,
|
|
||||||
userId: policy.userId,
|
|
||||||
icon: AccessSelectorComponent.userIcon,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
policies.groupAccessPolicies.forEach((policy) => {
|
private potentialGrantees$ = combineLatest([this.route.params]).pipe(
|
||||||
rows.push({
|
switchMap(([params]) =>
|
||||||
type: "group",
|
this.accessPolicyService
|
||||||
name: policy.groupName,
|
.getPeoplePotentialGrantees(params.organizationId)
|
||||||
id: policy.groupId,
|
.then((grantees) => {
|
||||||
accessPolicyId: policy.id,
|
return convertPotentialGranteesToApItemViewType(grantees);
|
||||||
read: policy.read,
|
})
|
||||||
write: policy.write,
|
)
|
||||||
currentUserInGroup: policy.currentUserInGroup,
|
);
|
||||||
icon: AccessSelectorComponent.groupIcon,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return rows;
|
|
||||||
}),
|
|
||||||
share()
|
|
||||||
);
|
|
||||||
|
|
||||||
protected handleCreateAccessPolicies(selected: SelectItemView[]) {
|
protected formGroup = new FormGroup({
|
||||||
const projectAccessPoliciesView = new ProjectAccessPoliciesView();
|
accessPolicies: new FormControl([] as ApItemValueType[]),
|
||||||
projectAccessPoliciesView.userAccessPolicies = selected
|
});
|
||||||
.filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "user")
|
|
||||||
.map((filtered) => {
|
|
||||||
const view = new UserProjectAccessPolicyView();
|
|
||||||
view.grantedProjectId = this.projectId;
|
|
||||||
view.organizationUserId = filtered.id;
|
|
||||||
view.read = true;
|
|
||||||
view.write = false;
|
|
||||||
return view;
|
|
||||||
});
|
|
||||||
|
|
||||||
projectAccessPoliciesView.groupAccessPolicies = selected
|
protected loading = true;
|
||||||
.filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "group")
|
protected potentialGrantees: ApItemViewType[];
|
||||||
.map((filtered) => {
|
|
||||||
const view = new GroupProjectAccessPolicyView();
|
|
||||||
view.grantedProjectId = this.projectId;
|
|
||||||
view.groupId = filtered.id;
|
|
||||||
view.read = true;
|
|
||||||
view.write = false;
|
|
||||||
return view;
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.accessPolicyService.createProjectAccessPolicies(
|
|
||||||
this.organizationId,
|
|
||||||
this.projectId,
|
|
||||||
projectAccessPoliciesView
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) {
|
|
||||||
if (
|
|
||||||
await this.accessPolicyService.needToShowAccessRemovalWarning(
|
|
||||||
this.organizationId,
|
|
||||||
policy,
|
|
||||||
this.rows
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.launchDeleteWarningDialog(policy);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.accessPolicyService.deleteAccessPolicy(policy.accessPolicyId);
|
|
||||||
} catch (e) {
|
|
||||||
this.validationService.showError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async handleUpdateAccessPolicy(policy: AccessSelectorRowView) {
|
|
||||||
if (
|
|
||||||
policy.read === true &&
|
|
||||||
policy.write === false &&
|
|
||||||
(await this.accessPolicyService.needToShowAccessRemovalWarning(
|
|
||||||
this.organizationId,
|
|
||||||
policy,
|
|
||||||
this.rows
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
this.launchUpdateWarningDialog(policy);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await this.accessPolicyService.updateAccessPolicy(
|
|
||||||
AccessSelectorComponent.getBaseAccessPolicyView(policy)
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
this.validationService.showError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private validationService: ValidationService,
|
private validationService: ValidationService,
|
||||||
private accessPolicyService: AccessPolicyService
|
private accessPolicyService: AccessPolicyService,
|
||||||
|
private router: Router,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
private i18nService: I18nService,
|
||||||
|
private accessPolicySelectorService: AccessPolicySelectorService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@ -154,9 +78,12 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy {
|
||||||
this.projectId = params.projectId;
|
this.projectId = params.projectId;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rows$.pipe(takeUntil(this.destroy$)).subscribe((rows) => {
|
combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
|
||||||
this.rows = rows;
|
.pipe(takeUntil(this.destroy$))
|
||||||
});
|
.subscribe(([potentialGrantees, currentAccessPolicies]) => {
|
||||||
|
this.potentialGrantees = potentialGrantees;
|
||||||
|
this.setSelected(currentAccessPolicies);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
@ -164,29 +91,80 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy {
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async launchDeleteWarningDialog(policy: AccessSelectorRowView) {
|
submit = async () => {
|
||||||
this.dialogService.open<unknown, AccessRemovalDetails>(AccessRemovalDialogComponent, {
|
this.formGroup.markAllAsTouched();
|
||||||
data: {
|
|
||||||
title: "smAccessRemovalWarningProjectTitle",
|
if (this.formGroup.invalid) {
|
||||||
message: "smAccessRemovalWarningProjectMessage",
|
return;
|
||||||
operation: "delete",
|
}
|
||||||
type: "project",
|
|
||||||
returnRoute: ["sm", this.organizationId, "projects"],
|
const showAccessRemovalWarning =
|
||||||
policy,
|
await this.accessPolicySelectorService.showAccessRemovalWarning(
|
||||||
},
|
this.organizationId,
|
||||||
});
|
this.formGroup.value.accessPolicies
|
||||||
|
);
|
||||||
|
|
||||||
|
if (showAccessRemovalWarning) {
|
||||||
|
const confirmed = await this.showWarning();
|
||||||
|
if (!confirmed) {
|
||||||
|
this.setSelected(this.currentAccessPolicies);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const projectPeopleView = convertToProjectPeopleAccessPoliciesView(
|
||||||
|
this.projectId,
|
||||||
|
this.formGroup.value.accessPolicies
|
||||||
|
);
|
||||||
|
const peoplePoliciesViews = await this.accessPolicyService.putProjectPeopleAccessPolicies(
|
||||||
|
this.projectId,
|
||||||
|
projectPeopleView
|
||||||
|
);
|
||||||
|
this.currentAccessPolicies = convertToAccessPolicyItemViews(peoplePoliciesViews);
|
||||||
|
|
||||||
|
if (showAccessRemovalWarning) {
|
||||||
|
this.router.navigate(["sm", this.organizationId, "projects"]);
|
||||||
|
}
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("projectAccessUpdated")
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
this.setSelected(this.currentAccessPolicies);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private setSelected(policiesToSelect: ApItemViewType[]) {
|
||||||
|
this.loading = true;
|
||||||
|
this.currentAccessPolicies = policiesToSelect;
|
||||||
|
if (policiesToSelect != undefined) {
|
||||||
|
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
|
||||||
|
// potentialGrantees, otherwise no selected values will be patched below
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
accessPolicies: policiesToSelect.map((m) => ({
|
||||||
|
type: m.type,
|
||||||
|
id: m.id,
|
||||||
|
permission: m.permission,
|
||||||
|
currentUser: m.type == ApItemEnum.User ? m.currentUser : null,
|
||||||
|
currentUserInGroup: m.type == ApItemEnum.Group ? m.currentUserInGroup : null,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private launchUpdateWarningDialog(policy: AccessSelectorRowView) {
|
private async showWarning(): Promise<boolean> {
|
||||||
this.dialogService.open<unknown, AccessRemovalDetails>(AccessRemovalDialogComponent, {
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
data: {
|
title: { key: "smAccessRemovalWarningProjectTitle" },
|
||||||
title: "smAccessRemovalWarningProjectTitle",
|
content: { key: "smAccessRemovalWarningProjectMessage" },
|
||||||
message: "smAccessRemovalWarningProjectMessage",
|
acceptButtonText: { key: "removeAccess" },
|
||||||
operation: "update",
|
cancelButtonText: { key: "cancel" },
|
||||||
type: "project",
|
type: "warning",
|
||||||
returnRoute: ["sm", this.organizationId, "projects"],
|
|
||||||
policy,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
return confirmed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
<div class="tw-flex">
|
||||||
|
<ng-container *ngIf="!addButtonMode; else buttonMode">
|
||||||
|
<bit-form-field class="tw-grow">
|
||||||
|
<bit-label>{{ label }}</bit-label>
|
||||||
|
<bit-multi-select
|
||||||
|
class="tw-w-full"
|
||||||
|
[loading]="loading"
|
||||||
|
[baseItems]="selectionList.deselectedItems"
|
||||||
|
[disabled]="disabled"
|
||||||
|
[removeSelectedItems]="true"
|
||||||
|
(blur)="handleBlur()"
|
||||||
|
(onItemsConfirmed)="selectItems($event)"
|
||||||
|
></bit-multi-select>
|
||||||
|
<bit-hint>{{ hint }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<bit-table [formGroup]="formGroup" *ngIf="!loading; else spinner">
|
||||||
|
<ng-container header>
|
||||||
|
<tr>
|
||||||
|
<th bitCell colspan="2">{{ columnTitle }}</th>
|
||||||
|
<th bitCell>{{ "permissions" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template body formArrayName="items">
|
||||||
|
<ng-container *ngIf="selectionList.selectedItems.length > 0; else empty">
|
||||||
|
<tr
|
||||||
|
bitRow
|
||||||
|
*ngFor="let item of selectionList.selectedItems; let i = index"
|
||||||
|
[formGroupName]="i"
|
||||||
|
>
|
||||||
|
<td bitCell class="tw-w-0 tw-pr-0">
|
||||||
|
<i class="bwi {{ item.icon }} tw-text-muted" aria-hidden="true"></i>
|
||||||
|
</td>
|
||||||
|
<td bitCell class="tw-max-w-sm tw-truncate">{{ item.labelName }}</td>
|
||||||
|
<td bitCell class="tw-mb-auto tw-inline-block tw-w-auto">
|
||||||
|
<select
|
||||||
|
*ngIf="!staticPermission; else static"
|
||||||
|
bitInput
|
||||||
|
formControlName="permission"
|
||||||
|
(blur)="handleBlur()"
|
||||||
|
>
|
||||||
|
<option *ngFor="let p of permissionList" [value]="p.perm">
|
||||||
|
{{ p.labelId | i18n }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<ng-template #static>
|
||||||
|
<span>{{ staticPermission | i18n }}</span>
|
||||||
|
</ng-template>
|
||||||
|
</td>
|
||||||
|
<td bitCell class="tw-w-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-close"
|
||||||
|
buttonType="main"
|
||||||
|
size="default"
|
||||||
|
[attr.title]="'remove' | i18n"
|
||||||
|
[attr.aria-label]="'remove' | i18n"
|
||||||
|
(click)="selectionList.deselectItem(item.id); handleBlur()"
|
||||||
|
></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</bit-table>
|
||||||
|
|
||||||
|
<ng-template #empty>
|
||||||
|
<div class="tw-mt-4 tw-text-center">
|
||||||
|
{{ emptyMessage }}
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #buttonMode>
|
||||||
|
<bit-form-field class="tw-grow" [formGroup]="multiSelectFormGroup">
|
||||||
|
<bit-label>{{ label }}</bit-label>
|
||||||
|
<bit-multi-select
|
||||||
|
class="tw-w-full"
|
||||||
|
formControlName="multiSelect"
|
||||||
|
[baseItems]="selectionList.deselectedItems"
|
||||||
|
(blur)="handleBlur()"
|
||||||
|
></bit-multi-select>
|
||||||
|
<bit-hint>{{ hint }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
|
<div class="tw-ml-3 tw-mt-7 tw-shrink-0">
|
||||||
|
<button type="button" bitButton buttonType="secondary" (click)="addButton()">
|
||||||
|
{{ "add" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #spinner>
|
||||||
|
<div class="tw-items-center tw-justify-center tw-pt-10 tw-text-center">
|
||||||
|
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,222 @@
|
||||||
|
import { Component, forwardRef, Input, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import {
|
||||||
|
ControlValueAccessor,
|
||||||
|
FormBuilder,
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
NG_VALUE_ACCESSOR,
|
||||||
|
} from "@angular/forms";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
|
||||||
|
import { FormSelectionList } from "@bitwarden/angular/utils/form-selection-list";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { SelectItemView } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { ApItemValueType } from "./models/ap-item-value.type";
|
||||||
|
import { ApItemViewType } from "./models/ap-item-view.type";
|
||||||
|
import { ApItemEnumUtil, ApItemEnum } from "./models/enums/ap-item.enum";
|
||||||
|
import { ApPermissionEnum } from "./models/enums/ap-permission.enum";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "sm-access-policy-selector",
|
||||||
|
templateUrl: "access-policy-selector.component.html",
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => AccessPolicySelectorComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AccessPolicySelectorComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private notifyOnChange: (v: unknown) => void;
|
||||||
|
private notifyOnTouch: () => void;
|
||||||
|
private pauseChangeNotification: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal selection list that tracks the value of this form control / component.
|
||||||
|
* It's responsible for keeping items sorted and synced with the rendered form controls
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected selectionList = new FormSelectionList<ApItemViewType, ApItemValueType>((item) => {
|
||||||
|
const initPermission = this.staticPermission ?? this.initialPermission;
|
||||||
|
|
||||||
|
const permissionControl = this.formBuilder.control(initPermission);
|
||||||
|
let currentUserInGroup = false;
|
||||||
|
let currentUser = false;
|
||||||
|
if (item.type == ApItemEnum.Group) {
|
||||||
|
currentUserInGroup = item.currentUserInGroup;
|
||||||
|
}
|
||||||
|
if (item.type == ApItemEnum.User) {
|
||||||
|
currentUser = item.currentUser;
|
||||||
|
}
|
||||||
|
const fg = this.formBuilder.group<ControlsOf<ApItemValueType>>({
|
||||||
|
id: new FormControl(item.id),
|
||||||
|
type: new FormControl(item.type),
|
||||||
|
permission: permissionControl,
|
||||||
|
currentUserInGroup: new FormControl(currentUserInGroup),
|
||||||
|
currentUser: new FormControl(currentUser),
|
||||||
|
});
|
||||||
|
return fg;
|
||||||
|
}, this._itemComparator.bind(this));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal form group for this component.
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected formGroup = this.formBuilder.group({
|
||||||
|
items: this.selectionList.formArray,
|
||||||
|
});
|
||||||
|
|
||||||
|
protected multiSelectFormGroup = new FormGroup({
|
||||||
|
multiSelect: new FormControl([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
disabled: boolean;
|
||||||
|
|
||||||
|
@Input() loading: boolean;
|
||||||
|
@Input() addButtonMode: boolean;
|
||||||
|
@Input() label: string;
|
||||||
|
@Input() hint: string;
|
||||||
|
@Input() columnTitle: string;
|
||||||
|
@Input() emptyMessage: string;
|
||||||
|
|
||||||
|
@Input() permissionList = [
|
||||||
|
{ perm: ApPermissionEnum.CanRead, labelId: "canRead" },
|
||||||
|
{ perm: ApPermissionEnum.CanReadWrite, labelId: "canReadWrite" },
|
||||||
|
];
|
||||||
|
@Input() initialPermission = ApPermissionEnum.CanRead;
|
||||||
|
|
||||||
|
// Pass in a static permission that wil be the only option for a given selector instance.
|
||||||
|
// Will ignore permissionList and initialPermission.
|
||||||
|
@Input() staticPermission: ApPermissionEnum;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
get items(): ApItemViewType[] {
|
||||||
|
return this.selectionList.allItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
set items(val: ApItemViewType[]) {
|
||||||
|
if (val != null) {
|
||||||
|
const selected = this.selectionList.formArray.getRawValue() ?? [];
|
||||||
|
this.selectionList.populateItems(
|
||||||
|
val.map((m) => {
|
||||||
|
m.icon = m.icon ?? ApItemEnumUtil.itemIcon(m.type);
|
||||||
|
return m;
|
||||||
|
}),
|
||||||
|
selected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly formBuilder: FormBuilder,
|
||||||
|
private readonly i18nService: I18nService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Required for NG_VALUE_ACCESSOR */
|
||||||
|
registerOnChange(fn: any): void {
|
||||||
|
this.notifyOnChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Required for NG_VALUE_ACCESSOR */
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
this.notifyOnTouch = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Required for NG_VALUE_ACCESSOR */
|
||||||
|
setDisabledState(isDisabled: boolean): void {
|
||||||
|
this.disabled = isDisabled;
|
||||||
|
|
||||||
|
// Keep the internal FormGroup in sync
|
||||||
|
if (this.disabled) {
|
||||||
|
this.formGroup.disable();
|
||||||
|
this.multiSelectFormGroup.disable();
|
||||||
|
} else {
|
||||||
|
this.formGroup.enable();
|
||||||
|
this.multiSelectFormGroup.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Required for NG_VALUE_ACCESSOR */
|
||||||
|
writeValue(selectedItems: ApItemValueType[]): void {
|
||||||
|
// Modifying the selection list, mistakenly fires valueChanges in the
|
||||||
|
// internal form array, so we need to know to pause external notification
|
||||||
|
this.pauseChangeNotification = true;
|
||||||
|
|
||||||
|
// Always clear the internal selection list on a new value
|
||||||
|
this.selectionList.deselectAll();
|
||||||
|
|
||||||
|
// If the new value is null, then we're done
|
||||||
|
if (selectedItems == null) {
|
||||||
|
this.pauseChangeNotification = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unable to handle other value types, throw
|
||||||
|
if (!Array.isArray(selectedItems)) {
|
||||||
|
throw new Error("The access selector component only supports Array form values!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate and internally select each item
|
||||||
|
for (const value of selectedItems) {
|
||||||
|
this.selectionList.selectItem(value.id, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pauseChangeNotification = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disabled form arrays emit values for disabled controls, we override this to emit an empty array to avoid
|
||||||
|
// emitting values for disabled controls that are "readonly" in the table
|
||||||
|
if (this.selectionList.formArray.disabled) {
|
||||||
|
this.notifyOnChange([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.notifyOnChange(v);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleBlur() {
|
||||||
|
if (!this.notifyOnTouch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyOnTouch();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected selectItems(items: SelectItemView[]) {
|
||||||
|
this.pauseChangeNotification = true;
|
||||||
|
this.selectionList.selectItems(items.map((i) => i.id));
|
||||||
|
this.pauseChangeNotification = false;
|
||||||
|
if (this.notifyOnChange != undefined) {
|
||||||
|
this.notifyOnChange(this.selectionList.formArray.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addButton() {
|
||||||
|
this.selectItems(this.multiSelectFormGroup.value.multiSelect);
|
||||||
|
this.multiSelectFormGroup.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _itemComparator(a: ApItemViewType, b: ApItemViewType) {
|
||||||
|
return (
|
||||||
|
a.type - b.type ||
|
||||||
|
this.i18nService.collator.compare(a.listName, b.listName) ||
|
||||||
|
this.i18nService.collator.compare(a.labelName, b.labelName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,240 @@
|
||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
|
||||||
|
import { AccessPolicySelectorService } from "./access-policy-selector.service";
|
||||||
|
import { ApItemValueType } from "./models/ap-item-value.type";
|
||||||
|
import { ApItemEnum } from "./models/enums/ap-item.enum";
|
||||||
|
import { ApPermissionEnum } from "./models/enums/ap-permission.enum";
|
||||||
|
|
||||||
|
describe("AccessPolicySelectorService", () => {
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
|
||||||
|
let sut: AccessPolicySelectorService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
|
||||||
|
sut = new AccessPolicySelectorService(organizationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => jest.resetAllMocks());
|
||||||
|
|
||||||
|
describe("showAccessRemovalWarning", () => {
|
||||||
|
it("returns false when current user is admin", async () => {
|
||||||
|
const org = orgFactory();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when current user is owner", async () => {
|
||||||
|
const org = orgFactory();
|
||||||
|
org.type = OrganizationUserType.Owner;
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when current user isn't owner/admin and all policies are removed", async () => {
|
||||||
|
const org = setupUserOrg();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when current user isn't owner/admin and user policy is set to canRead", async () => {
|
||||||
|
const org = setupUserOrg();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [];
|
||||||
|
selectedPolicyValues.push(
|
||||||
|
createApItemValueType({
|
||||||
|
permission: ApPermissionEnum.CanRead,
|
||||||
|
currentUser: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when current user isn't owner/admin and user policy is set to canReadWrite", async () => {
|
||||||
|
const org = setupUserOrg();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [
|
||||||
|
createApItemValueType({
|
||||||
|
permission: ApPermissionEnum.CanReadWrite,
|
||||||
|
currentUser: true,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when current user isn't owner/admin and a group Read policy is submitted that the user is a member of", async () => {
|
||||||
|
const org = setupUserOrg();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [
|
||||||
|
createApItemValueType({
|
||||||
|
id: "groupId",
|
||||||
|
type: ApItemEnum.Group,
|
||||||
|
permission: ApPermissionEnum.CanRead,
|
||||||
|
currentUserInGroup: true,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when current user isn't owner/admin and a group ReadWrite policy is submitted that the user is a member of", async () => {
|
||||||
|
const org = setupUserOrg();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [
|
||||||
|
createApItemValueType({
|
||||||
|
id: "groupId",
|
||||||
|
type: ApItemEnum.Group,
|
||||||
|
permission: ApPermissionEnum.CanReadWrite,
|
||||||
|
currentUserInGroup: true,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when current user isn't owner/admin and a group ReadWrite policy is submitted that the user is not a member of", async () => {
|
||||||
|
const org = setupUserOrg();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [
|
||||||
|
createApItemValueType({
|
||||||
|
id: "groupId",
|
||||||
|
type: ApItemEnum.Group,
|
||||||
|
permission: ApPermissionEnum.CanReadWrite,
|
||||||
|
currentUserInGroup: false,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when current user isn't owner/admin, user policy is set to CanRead, and user is in read write group", async () => {
|
||||||
|
const org = setupUserOrg();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [
|
||||||
|
createApItemValueType({
|
||||||
|
permission: ApPermissionEnum.CanRead,
|
||||||
|
currentUser: true,
|
||||||
|
}),
|
||||||
|
createApItemValueType({
|
||||||
|
id: "groupId",
|
||||||
|
type: ApItemEnum.Group,
|
||||||
|
permission: ApPermissionEnum.CanReadWrite,
|
||||||
|
currentUserInGroup: true,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when current user isn't owner/admin, user policy is set to CanRead, and user is not in ReadWrite group", async () => {
|
||||||
|
const org = setupUserOrg();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [
|
||||||
|
createApItemValueType({
|
||||||
|
permission: ApPermissionEnum.CanRead,
|
||||||
|
currentUser: true,
|
||||||
|
}),
|
||||||
|
createApItemValueType({
|
||||||
|
id: "groupId",
|
||||||
|
type: ApItemEnum.Group,
|
||||||
|
permission: ApPermissionEnum.CanReadWrite,
|
||||||
|
currentUserInGroup: false,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when current user isn't owner/admin, user policy is set to CanRead, and user is in Read group", async () => {
|
||||||
|
const org = setupUserOrg();
|
||||||
|
organizationService.get.calledWith(org.id).mockReturnValue(org);
|
||||||
|
|
||||||
|
const selectedPolicyValues: ApItemValueType[] = [
|
||||||
|
createApItemValueType({
|
||||||
|
permission: ApPermissionEnum.CanRead,
|
||||||
|
currentUser: true,
|
||||||
|
}),
|
||||||
|
createApItemValueType({
|
||||||
|
id: "groupId",
|
||||||
|
type: ApItemEnum.Group,
|
||||||
|
permission: ApPermissionEnum.CanRead,
|
||||||
|
currentUserInGroup: true,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const orgFactory = (props: Partial<Organization> = {}) =>
|
||||||
|
Object.assign(
|
||||||
|
new Organization(),
|
||||||
|
{
|
||||||
|
id: "myOrgId",
|
||||||
|
enabled: true,
|
||||||
|
type: OrganizationUserType.Admin,
|
||||||
|
},
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
function createApItemValueType(options: Partial<ApItemValueType> = {}) {
|
||||||
|
return {
|
||||||
|
id: options?.id ?? "test",
|
||||||
|
type: options?.type ?? ApItemEnum.User,
|
||||||
|
permission: options?.permission ?? ApPermissionEnum.CanRead,
|
||||||
|
currentUserInGroup: options?.currentUserInGroup ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupUserOrg() {
|
||||||
|
const userId = "testUserId";
|
||||||
|
const org = orgFactory({ userId: userId });
|
||||||
|
org.type = OrganizationUserType.User;
|
||||||
|
return org;
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
|
||||||
|
import { ApItemValueType } from "./models/ap-item-value.type";
|
||||||
|
import { ApItemEnum } from "./models/enums/ap-item.enum";
|
||||||
|
import { ApPermissionEnum } from "./models/enums/ap-permission.enum";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class AccessPolicySelectorService {
|
||||||
|
constructor(private organizationService: OrganizationService) {}
|
||||||
|
|
||||||
|
async showAccessRemovalWarning(
|
||||||
|
organizationId: string,
|
||||||
|
selectedPoliciesValues: ApItemValueType[]
|
||||||
|
): Promise<boolean> {
|
||||||
|
const organization = this.organizationService.get(organizationId);
|
||||||
|
if (organization.isOwner || organization.isAdmin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedUserReadWritePolicy = selectedPoliciesValues.find(
|
||||||
|
(s) =>
|
||||||
|
s.type === ApItemEnum.User &&
|
||||||
|
s.currentUser &&
|
||||||
|
s.permission === ApPermissionEnum.CanReadWrite
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedGroupReadWritePolicies = selectedPoliciesValues.filter(
|
||||||
|
(s) =>
|
||||||
|
s.type === ApItemEnum.Group &&
|
||||||
|
s.permission == ApPermissionEnum.CanReadWrite &&
|
||||||
|
s.currentUserInGroup
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedGroupReadWritePolicies == null || selectedGroupReadWritePolicies.length == 0) {
|
||||||
|
if (selectedUserReadWritePolicy == null) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import {
|
||||||
|
ProjectPeopleAccessPoliciesView,
|
||||||
|
UserProjectAccessPolicyView,
|
||||||
|
GroupProjectAccessPolicyView,
|
||||||
|
} from "../../../../models/view/access-policy.view";
|
||||||
|
|
||||||
|
import { ApItemEnum } from "./enums/ap-item.enum";
|
||||||
|
import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum";
|
||||||
|
|
||||||
|
export type ApItemValueType = {
|
||||||
|
id: string;
|
||||||
|
type: ApItemEnum;
|
||||||
|
permission: ApPermissionEnum;
|
||||||
|
currentUserInGroup?: boolean;
|
||||||
|
currentUser?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convertToProjectPeopleAccessPoliciesView(
|
||||||
|
projectId: string,
|
||||||
|
selectedPolicyValues: ApItemValueType[]
|
||||||
|
): ProjectPeopleAccessPoliciesView {
|
||||||
|
const view = new ProjectPeopleAccessPoliciesView();
|
||||||
|
view.userAccessPolicies = selectedPolicyValues
|
||||||
|
.filter((x) => x.type == ApItemEnum.User)
|
||||||
|
.map((filtered) => {
|
||||||
|
const policyView = new UserProjectAccessPolicyView();
|
||||||
|
policyView.grantedProjectId = projectId;
|
||||||
|
policyView.organizationUserId = filtered.id;
|
||||||
|
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission);
|
||||||
|
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission);
|
||||||
|
return policyView;
|
||||||
|
});
|
||||||
|
|
||||||
|
view.groupAccessPolicies = selectedPolicyValues
|
||||||
|
.filter((x) => x.type == ApItemEnum.Group)
|
||||||
|
.map((filtered) => {
|
||||||
|
const policyView = new GroupProjectAccessPolicyView();
|
||||||
|
policyView.grantedProjectId = projectId;
|
||||||
|
policyView.groupId = filtered.id;
|
||||||
|
policyView.read = ApPermissionEnumUtil.toRead(filtered.permission);
|
||||||
|
policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission);
|
||||||
|
return policyView;
|
||||||
|
});
|
||||||
|
return view;
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { SelectItemView } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policy.view";
|
||||||
|
import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view";
|
||||||
|
|
||||||
|
import { ApItemEnum, ApItemEnumUtil } from "./enums/ap-item.enum";
|
||||||
|
import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum";
|
||||||
|
|
||||||
|
export type ApItemViewType =
|
||||||
|
| SelectItemView & {
|
||||||
|
accessPolicyId?: string;
|
||||||
|
permission?: ApPermissionEnum;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: ApItemEnum.User;
|
||||||
|
userId?: string;
|
||||||
|
currentUser?: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ApItemEnum.Group;
|
||||||
|
currentUserInGroup?: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ApItemEnum.ServiceAccount;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ApItemEnum.Project;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function convertToAccessPolicyItemViews(
|
||||||
|
value: ProjectPeopleAccessPoliciesView
|
||||||
|
): ApItemViewType[] {
|
||||||
|
const accessPolicies: ApItemViewType[] = [];
|
||||||
|
|
||||||
|
value.userAccessPolicies.forEach((policy) => {
|
||||||
|
accessPolicies.push({
|
||||||
|
type: ApItemEnum.User,
|
||||||
|
icon: ApItemEnumUtil.itemIcon(ApItemEnum.User),
|
||||||
|
id: policy.organizationUserId,
|
||||||
|
accessPolicyId: policy.id,
|
||||||
|
labelName: policy.organizationUserName,
|
||||||
|
listName: policy.organizationUserName,
|
||||||
|
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
|
||||||
|
userId: policy.userId,
|
||||||
|
currentUser: policy.currentUser,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
value.groupAccessPolicies.forEach((policy) => {
|
||||||
|
accessPolicies.push({
|
||||||
|
type: ApItemEnum.Group,
|
||||||
|
icon: ApItemEnumUtil.itemIcon(ApItemEnum.Group),
|
||||||
|
id: policy.groupId,
|
||||||
|
accessPolicyId: policy.id,
|
||||||
|
labelName: policy.groupName,
|
||||||
|
listName: policy.groupName,
|
||||||
|
permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write),
|
||||||
|
currentUserInGroup: policy.currentUserInGroup,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return accessPolicies;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertPotentialGranteesToApItemViewType(
|
||||||
|
grantees: PotentialGranteeView[]
|
||||||
|
): ApItemViewType[] {
|
||||||
|
return grantees.map((granteeView) => {
|
||||||
|
let icon: string;
|
||||||
|
let type: ApItemEnum;
|
||||||
|
let listName = granteeView.name;
|
||||||
|
let labelName = granteeView.name;
|
||||||
|
|
||||||
|
switch (granteeView.type) {
|
||||||
|
case "user":
|
||||||
|
icon = ApItemEnumUtil.itemIcon(ApItemEnum.User);
|
||||||
|
type = ApItemEnum.User;
|
||||||
|
if (Utils.isNullOrWhitespace(granteeView.name)) {
|
||||||
|
listName = granteeView.email;
|
||||||
|
labelName = granteeView.email;
|
||||||
|
} else {
|
||||||
|
listName = `${granteeView.name} (${granteeView.email})`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "group":
|
||||||
|
icon = ApItemEnumUtil.itemIcon(ApItemEnum.Group);
|
||||||
|
type = ApItemEnum.Group;
|
||||||
|
break;
|
||||||
|
case "serviceAccount":
|
||||||
|
icon = ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount);
|
||||||
|
type = ApItemEnum.ServiceAccount;
|
||||||
|
break;
|
||||||
|
case "project":
|
||||||
|
icon = ApItemEnumUtil.itemIcon(ApItemEnum.Project);
|
||||||
|
type = ApItemEnum.Project;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: icon,
|
||||||
|
type: type,
|
||||||
|
id: granteeView.id,
|
||||||
|
labelName: labelName,
|
||||||
|
listName: listName,
|
||||||
|
currentUserInGroup: granteeView.currentUserInGroup,
|
||||||
|
currentUser: granteeView.currentUser,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
export enum ApItemEnum {
|
||||||
|
User,
|
||||||
|
Group,
|
||||||
|
ServiceAccount,
|
||||||
|
Project,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApItemEnumUtil {
|
||||||
|
static itemIcon(type: ApItemEnum): string {
|
||||||
|
switch (type) {
|
||||||
|
case ApItemEnum.User:
|
||||||
|
return "bwi-user";
|
||||||
|
case ApItemEnum.Group:
|
||||||
|
return "bwi-family";
|
||||||
|
case ApItemEnum.ServiceAccount:
|
||||||
|
return "bwi-wrench";
|
||||||
|
case ApItemEnum.Project:
|
||||||
|
return "bwi-collection";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
export enum ApPermissionEnum {
|
||||||
|
CanRead = "canRead",
|
||||||
|
CanReadWrite = "canReadWrite",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApPermissionEnumUtil {
|
||||||
|
static toApPermissionEnum(read: boolean, write: boolean): ApPermissionEnum {
|
||||||
|
if (read && write) {
|
||||||
|
return ApPermissionEnum.CanReadWrite;
|
||||||
|
} else if (read) {
|
||||||
|
return ApPermissionEnum.CanRead;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported Access Policy Permission option");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static toRead(permission: ApPermissionEnum): boolean {
|
||||||
|
if (permission == ApPermissionEnum.CanRead || permission == ApPermissionEnum.CanReadWrite) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static toWrite(permission: ApPermissionEnum): boolean {
|
||||||
|
if (permission === ApPermissionEnum.CanReadWrite) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import {
|
||||||
GroupProjectAccessPolicyView,
|
GroupProjectAccessPolicyView,
|
||||||
GroupServiceAccountAccessPolicyView,
|
GroupServiceAccountAccessPolicyView,
|
||||||
ProjectAccessPoliciesView,
|
ProjectAccessPoliciesView,
|
||||||
|
ProjectPeopleAccessPoliciesView,
|
||||||
ServiceAccountAccessPoliciesView,
|
ServiceAccountAccessPoliciesView,
|
||||||
ServiceAccountProjectAccessPolicyView,
|
ServiceAccountProjectAccessPolicyView,
|
||||||
UserProjectAccessPolicyView,
|
UserProjectAccessPolicyView,
|
||||||
|
@ -21,6 +22,7 @@ import {
|
||||||
} from "../../models/view/access-policy.view";
|
} from "../../models/view/access-policy.view";
|
||||||
import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
|
import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
|
||||||
import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request";
|
import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request";
|
||||||
|
import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request";
|
||||||
import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response";
|
import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response";
|
||||||
import { ServiceAccountAccessPoliciesResponse } from "../../shared/access-policies/models/responses/service-accounts-access-policies.response";
|
import { ServiceAccountAccessPoliciesResponse } from "../../shared/access-policies/models/responses/service-accounts-access-policies.response";
|
||||||
|
|
||||||
|
@ -36,6 +38,7 @@ import {
|
||||||
UserProjectAccessPolicyResponse,
|
UserProjectAccessPolicyResponse,
|
||||||
} from "./models/responses/access-policy.response";
|
} from "./models/responses/access-policy.response";
|
||||||
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
|
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
|
||||||
|
import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
|
@ -133,6 +136,37 @@ export class AccessPolicyService {
|
||||||
return await this.createProjectAccessPoliciesView(organizationId, results);
|
return await this.createProjectAccessPoliciesView(organizationId, results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectPeopleAccessPolicies(
|
||||||
|
projectId: string
|
||||||
|
): Promise<ProjectPeopleAccessPoliciesView> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/projects/" + projectId + "/access-policies/people",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = new ProjectPeopleAccessPoliciesResponse(r);
|
||||||
|
return this.createProjectPeopleAccessPoliciesView(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async putProjectPeopleAccessPolicies(
|
||||||
|
projectId: string,
|
||||||
|
peoplePoliciesView: ProjectPeopleAccessPoliciesView
|
||||||
|
) {
|
||||||
|
const request = this.getPeopleAccessPoliciesRequest(peoplePoliciesView);
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"PUT",
|
||||||
|
"/projects/" + projectId + "/access-policies/people",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const results = new ProjectPeopleAccessPoliciesResponse(r);
|
||||||
|
return this.createProjectPeopleAccessPoliciesView(results);
|
||||||
|
}
|
||||||
|
|
||||||
async getServiceAccountAccessPolicies(
|
async getServiceAccountAccessPolicies(
|
||||||
serviceAccountId: string
|
serviceAccountId: string
|
||||||
): Promise<ServiceAccountAccessPoliciesView> {
|
): Promise<ServiceAccountAccessPoliciesView> {
|
||||||
|
@ -258,6 +292,20 @@ export class AccessPolicyService {
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createProjectPeopleAccessPoliciesView(
|
||||||
|
peopleAccessPoliciesResponse: ProjectPeopleAccessPoliciesResponse
|
||||||
|
): ProjectPeopleAccessPoliciesView {
|
||||||
|
const view = new ProjectPeopleAccessPoliciesView();
|
||||||
|
|
||||||
|
view.userAccessPolicies = peopleAccessPoliciesResponse.userAccessPolicies.map((ap) => {
|
||||||
|
return this.createUserProjectAccessPolicyView(ap);
|
||||||
|
});
|
||||||
|
view.groupAccessPolicies = peopleAccessPoliciesResponse.groupAccessPolicies.map((ap) => {
|
||||||
|
return this.createGroupProjectAccessPolicyView(ap);
|
||||||
|
});
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
private getAccessPoliciesCreateRequest(
|
private getAccessPoliciesCreateRequest(
|
||||||
projectAccessPoliciesView: ProjectAccessPoliciesView
|
projectAccessPoliciesView: ProjectAccessPoliciesView
|
||||||
): AccessPoliciesCreateRequest {
|
): AccessPoliciesCreateRequest {
|
||||||
|
@ -288,6 +336,30 @@ export class AccessPolicyService {
|
||||||
return createRequest;
|
return createRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPeopleAccessPoliciesRequest(
|
||||||
|
projectPeopleAccessPoliciesView: ProjectPeopleAccessPoliciesView
|
||||||
|
): PeopleAccessPoliciesRequest {
|
||||||
|
const request = new PeopleAccessPoliciesRequest();
|
||||||
|
|
||||||
|
if (projectPeopleAccessPoliciesView.userAccessPolicies?.length > 0) {
|
||||||
|
request.userAccessPolicyRequests = projectPeopleAccessPoliciesView.userAccessPolicies.map(
|
||||||
|
(ap) => {
|
||||||
|
return this.getAccessPolicyRequest(ap.organizationUserId, ap);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectPeopleAccessPoliciesView.groupAccessPolicies?.length > 0) {
|
||||||
|
request.groupAccessPolicyRequests = projectPeopleAccessPoliciesView.groupAccessPolicies.map(
|
||||||
|
(ap) => {
|
||||||
|
return this.getAccessPolicyRequest(ap.groupId, ap);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
private createUserProjectAccessPolicyView(
|
private createUserProjectAccessPolicyView(
|
||||||
response: UserProjectAccessPolicyResponse
|
response: UserProjectAccessPolicyResponse
|
||||||
): UserProjectAccessPolicyView {
|
): UserProjectAccessPolicyView {
|
||||||
|
@ -297,6 +369,7 @@ export class AccessPolicyService {
|
||||||
organizationUserId: response.organizationUserId,
|
organizationUserId: response.organizationUserId,
|
||||||
organizationUserName: response.organizationUserName,
|
organizationUserName: response.organizationUserName,
|
||||||
userId: response.userId,
|
userId: response.userId,
|
||||||
|
currentUser: response.currentUser,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -478,6 +551,8 @@ export class AccessPolicyService {
|
||||||
view.id = r.id;
|
view.id = r.id;
|
||||||
view.type = r.type;
|
view.type = r.type;
|
||||||
view.email = r.email;
|
view.email = r.email;
|
||||||
|
view.currentUser = r.currentUser;
|
||||||
|
view.currentUserInGroup = r.currentUserInGroup;
|
||||||
|
|
||||||
if (r.type === "serviceAccount" || r.type === "project") {
|
if (r.type === "serviceAccount" || r.type === "project") {
|
||||||
view.name = await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey);
|
view.name = await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey);
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { AccessPolicyRequest } from "./access-policy.request";
|
||||||
|
|
||||||
|
export class PeopleAccessPoliciesRequest {
|
||||||
|
userAccessPolicyRequests?: AccessPolicyRequest[];
|
||||||
|
groupAccessPolicyRequests?: AccessPolicyRequest[];
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ export class UserProjectAccessPolicyResponse extends BaseAccessPolicyResponse {
|
||||||
organizationUserName: string;
|
organizationUserName: string;
|
||||||
grantedProjectId: string;
|
grantedProjectId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
currentUser: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
|
@ -29,6 +30,7 @@ export class UserProjectAccessPolicyResponse extends BaseAccessPolicyResponse {
|
||||||
this.organizationUserName = this.getResponseProperty("OrganizationUserName");
|
this.organizationUserName = this.getResponseProperty("OrganizationUserName");
|
||||||
this.grantedProjectId = this.getResponseProperty("GrantedProjectId");
|
this.grantedProjectId = this.getResponseProperty("GrantedProjectId");
|
||||||
this.userId = this.getResponseProperty("UserId");
|
this.userId = this.getResponseProperty("UserId");
|
||||||
|
this.currentUser = this.getResponseProperty("CurrentUser");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ export class PotentialGranteeResponse extends BaseResponse {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
currentUserInGroup: boolean;
|
||||||
|
currentUser: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
|
@ -12,5 +14,7 @@ export class PotentialGranteeResponse extends BaseResponse {
|
||||||
this.name = this.getResponseProperty("Name");
|
this.name = this.getResponseProperty("Name");
|
||||||
this.type = this.getResponseProperty("Type");
|
this.type = this.getResponseProperty("Type");
|
||||||
this.email = this.getResponseProperty("Email");
|
this.email = this.getResponseProperty("Email");
|
||||||
|
this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup");
|
||||||
|
this.currentUser = this.getResponseProperty("CurrentUser");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||||
|
|
||||||
|
import {
|
||||||
|
GroupProjectAccessPolicyResponse,
|
||||||
|
UserProjectAccessPolicyResponse,
|
||||||
|
} from "./access-policy.response";
|
||||||
|
|
||||||
|
export class ProjectPeopleAccessPoliciesResponse extends BaseResponse {
|
||||||
|
userAccessPolicies: UserProjectAccessPolicyResponse[];
|
||||||
|
groupAccessPolicies: GroupProjectAccessPolicyResponse[];
|
||||||
|
|
||||||
|
constructor(response: any) {
|
||||||
|
super(response);
|
||||||
|
const userAccessPolicies = this.getResponseProperty("UserAccessPolicies");
|
||||||
|
this.userAccessPolicies = userAccessPolicies.map(
|
||||||
|
(k: any) => new UserProjectAccessPolicyResponse(k)
|
||||||
|
);
|
||||||
|
const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies");
|
||||||
|
this.groupAccessPolicies = groupAccessPolicies.map(
|
||||||
|
(k: any) => new GroupProjectAccessPolicyResponse(k)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { DynamicAvatarComponent } from "@bitwarden/web-vault/app/components/dyna
|
||||||
import { ProductSwitcherModule } from "@bitwarden/web-vault/app/layouts/product-switcher/product-switcher.module";
|
import { ProductSwitcherModule } from "@bitwarden/web-vault/app/layouts/product-switcher/product-switcher.module";
|
||||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||||
|
|
||||||
|
import { AccessPolicySelectorComponent } from "./access-policies/access-policy-selector/access-policy-selector.component";
|
||||||
import { AccessSelectorComponent } from "./access-policies/access-selector.component";
|
import { AccessSelectorComponent } from "./access-policies/access-selector.component";
|
||||||
import { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-removal-dialog.component";
|
import { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-removal-dialog.component";
|
||||||
import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component";
|
import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component";
|
||||||
|
@ -37,6 +38,7 @@ import { SecretsListComponent } from "./secrets-list.component";
|
||||||
NoItemsModule,
|
NoItemsModule,
|
||||||
AccessRemovalDialogComponent,
|
AccessRemovalDialogComponent,
|
||||||
AccessSelectorComponent,
|
AccessSelectorComponent,
|
||||||
|
AccessPolicySelectorComponent,
|
||||||
BulkStatusDialogComponent,
|
BulkStatusDialogComponent,
|
||||||
BulkConfirmationDialogComponent,
|
BulkConfirmationDialogComponent,
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
|
@ -57,6 +59,7 @@ import { SecretsListComponent } from "./secrets-list.component";
|
||||||
SecretsListComponent,
|
SecretsListComponent,
|
||||||
AccessSelectorComponent,
|
AccessSelectorComponent,
|
||||||
OrgSuspendedComponent,
|
OrgSuspendedComponent,
|
||||||
|
AccessPolicySelectorComponent,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
bootstrap: [],
|
bootstrap: [],
|
||||||
|
|
Loading…
Reference in New Issue