From ec205d4224fb687f9717642ea5b10fe9ecfebc6d Mon Sep 17 00:00:00 2001
From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Date: Wed, 8 Nov 2023 11:41:41 -0500
Subject: [PATCH] [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)
---
apps/web/src/locales/en/messages.json | 3 +
.../models/view/access-policy.view.ts | 6 +
.../models/view/potential-grantee.view.ts | 2 +
.../project/project-people.component.html | 38 +--
.../project/project-people.component.ts | 276 ++++++++----------
.../access-policy-selector.component.html | 97 ++++++
.../access-policy-selector.component.ts | 222 ++++++++++++++
.../access-policy-selector.service.spec.ts | 240 +++++++++++++++
.../access-policy-selector.service.ts | 48 +++
.../models/ap-item-value.type.ts | 45 +++
.../models/ap-item-view.type.ts | 111 +++++++
.../models/enums/ap-item.enum.ts | 21 ++
.../models/enums/ap-permission.enum.ts | 30 ++
.../access-policies/access-policy.service.ts | 75 +++++
.../people-access-policies.request.ts | 6 +
.../responses/access-policy.response.ts | 2 +
.../responses/potential-grantee.response.ts | 4 +
...project-people-access-policies.response.ts | 23 ++
.../shared/sm-shared.module.ts | 3 +
19 files changed, 1086 insertions(+), 166 deletions(-)
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.ts
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-item.enum.ts
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-permission.enum.ts
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/people-access-policies.request.ts
create mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-people-access-policies.response.ts
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 0a831680f4c..7aface16412 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -7386,5 +7386,8 @@
"seeDetailedInstructions": {
"message": "See detailed instructions on our help site at",
"description": "This is followed a by a hyperlink to the help website."
+ },
+ "projectAccessUpdated": {
+ "message": "Project access updated"
}
}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts
index ac6edfe39e1..1b102acaa5e 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/access-policy.view.ts
@@ -11,6 +11,7 @@ export class UserProjectAccessPolicyView extends BaseAccessPolicyView {
organizationUserName: string;
grantedProjectId: string;
userId: string;
+ currentUser: boolean;
}
export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView {
@@ -47,6 +48,11 @@ export class ProjectAccessPoliciesView {
serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyView[];
}
+export class ProjectPeopleAccessPoliciesView {
+ userAccessPolicies: UserProjectAccessPolicyView[];
+ groupAccessPolicies: GroupProjectAccessPolicyView[];
+}
+
export class ServiceAccountAccessPoliciesView {
userAccessPolicies: UserServiceAccountAccessPolicyView[];
groupAccessPolicies: GroupServiceAccountAccessPolicyView[];
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/potential-grantee.view.ts b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/potential-grantee.view.ts
index a3bf9827d55..5a6af6eb13c 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/models/view/potential-grantee.view.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/models/view/potential-grantee.view.ts
@@ -3,4 +3,6 @@ export class PotentialGranteeView {
name: string;
type: string;
email: string;
+ currentUserInGroup: boolean;
+ currentUser: boolean;
}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html
index 0485c5df1fa..9a47f426869 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.html
@@ -1,17 +1,21 @@
-
-
- {{ "projectPeopleDescription" | i18n }}
-
-
-
-
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts
index 03b2625f001..661b3364825 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/projects/project/project-people.component.ts
@@ -1,151 +1,75 @@
-import { Component, OnDestroy, OnInit } from "@angular/core";
-import { ActivatedRoute } from "@angular/router";
-import { map, Observable, share, startWith, Subject, switchMap, takeUntil } from "rxjs";
+import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
+import { FormControl, FormGroup } from "@angular/forms";
+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 { DialogService, SelectItemView } from "@bitwarden/components";
+import { DialogService } from "@bitwarden/components";
+import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service";
import {
- GroupProjectAccessPolicyView,
- ProjectAccessPoliciesView,
- UserProjectAccessPolicyView,
-} from "../../models/view/access-policy.view";
+ ApItemValueType,
+ convertToProjectPeopleAccessPoliciesView,
+} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
+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 {
- AccessSelectorComponent,
- AccessSelectorRowView,
-} from "../../shared/access-policies/access-selector.component";
-import {
- AccessRemovalDetails,
- AccessRemovalDialogComponent,
-} from "../../shared/access-policies/dialogs/access-removal-dialog.component";
@Component({
selector: "sm-project-people",
templateUrl: "./project-people.component.html",
})
export class ProjectPeopleComponent implements OnInit, OnDestroy {
+ private currentAccessPolicies: ApItemViewType[];
private destroy$ = new Subject();
private organizationId: string;
private projectId: string;
- private rows: AccessSelectorRowView[];
- protected rows$: Observable =
- this.accessPolicyService.projectAccessPolicyChanges$.pipe(
- startWith(null),
- switchMap(() =>
- this.accessPolicyService.getProjectAccessPolicies(this.organizationId, this.projectId)
- ),
- map((policies) => {
- const rows: AccessSelectorRowView[] = [];
- policies.userAccessPolicies.forEach((policy) => {
- 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,
- });
- });
+ private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
+ switchMap(([params]) =>
+ this.accessPolicyService.getProjectPeopleAccessPolicies(params.projectId).then((policies) => {
+ return convertToAccessPolicyItemViews(policies);
+ })
+ ),
+ catchError(() => {
+ this.router.navigate(["/sm", this.organizationId, "projects"]);
+ return EMPTY;
+ })
+ );
- policies.groupAccessPolicies.forEach((policy) => {
- rows.push({
- type: "group",
- name: policy.groupName,
- id: policy.groupId,
- accessPolicyId: policy.id,
- read: policy.read,
- write: policy.write,
- currentUserInGroup: policy.currentUserInGroup,
- icon: AccessSelectorComponent.groupIcon,
- });
- });
- return rows;
- }),
- share()
- );
+ private potentialGrantees$ = combineLatest([this.route.params]).pipe(
+ switchMap(([params]) =>
+ this.accessPolicyService
+ .getPeoplePotentialGrantees(params.organizationId)
+ .then((grantees) => {
+ return convertPotentialGranteesToApItemViewType(grantees);
+ })
+ )
+ );
- protected handleCreateAccessPolicies(selected: SelectItemView[]) {
- const projectAccessPoliciesView = new ProjectAccessPoliciesView();
- 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;
- });
+ protected formGroup = new FormGroup({
+ accessPolicies: new FormControl([] as ApItemValueType[]),
+ });
- projectAccessPoliciesView.groupAccessPolicies = selected
- .filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "group")
- .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);
- }
- }
+ protected loading = true;
+ protected potentialGrantees: ApItemViewType[];
constructor(
private route: ActivatedRoute,
private dialogService: DialogService,
+ private changeDetectorRef: ChangeDetectorRef,
private validationService: ValidationService,
- private accessPolicyService: AccessPolicyService
+ private accessPolicyService: AccessPolicyService,
+ private router: Router,
+ private platformUtilsService: PlatformUtilsService,
+ private i18nService: I18nService,
+ private accessPolicySelectorService: AccessPolicySelectorService
) {}
ngOnInit(): void {
@@ -154,9 +78,12 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy {
this.projectId = params.projectId;
});
- this.rows$.pipe(takeUntil(this.destroy$)).subscribe((rows) => {
- this.rows = rows;
- });
+ combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(([potentialGrantees, currentAccessPolicies]) => {
+ this.potentialGrantees = potentialGrantees;
+ this.setSelected(currentAccessPolicies);
+ });
}
ngOnDestroy(): void {
@@ -164,29 +91,80 @@ export class ProjectPeopleComponent implements OnInit, OnDestroy {
this.destroy$.complete();
}
- private async launchDeleteWarningDialog(policy: AccessSelectorRowView) {
- this.dialogService.open(AccessRemovalDialogComponent, {
- data: {
- title: "smAccessRemovalWarningProjectTitle",
- message: "smAccessRemovalWarningProjectMessage",
- operation: "delete",
- type: "project",
- returnRoute: ["sm", this.organizationId, "projects"],
- policy,
- },
- });
+ submit = async () => {
+ this.formGroup.markAllAsTouched();
+
+ if (this.formGroup.invalid) {
+ return;
+ }
+
+ const showAccessRemovalWarning =
+ 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) {
- this.dialogService.open(AccessRemovalDialogComponent, {
- data: {
- title: "smAccessRemovalWarningProjectTitle",
- message: "smAccessRemovalWarningProjectMessage",
- operation: "update",
- type: "project",
- returnRoute: ["sm", this.organizationId, "projects"],
- policy,
- },
+ private async showWarning(): Promise {
+ const confirmed = await this.dialogService.openSimpleDialog({
+ title: { key: "smAccessRemovalWarningProjectTitle" },
+ content: { key: "smAccessRemovalWarningProjectMessage" },
+ acceptButtonText: { key: "removeAccess" },
+ cancelButtonText: { key: "cancel" },
+ type: "warning",
});
+ return confirmed;
}
}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html
new file mode 100644
index 00000000000..4b3c8392641
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.html
@@ -0,0 +1,97 @@
+
+
+
+ {{ label }}
+
+ {{ hint }}
+
+
+
+
+
+
+
+ | {{ columnTitle }} |
+ {{ "permissions" | i18n }} |
+
+
+
+ 0; else empty">
+
+ |
+
+ |
+ {{ item.labelName }} |
+
+
+
+ {{ staticPermission | i18n }}
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+ {{ emptyMessage }}
+
+
+
+
+
+ {{ label }}
+
+ {{ hint }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts
new file mode 100644
index 00000000000..2bfea23fe06
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts
@@ -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();
+ 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((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>({
+ 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)
+ );
+ }
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts
new file mode 100644
index 00000000000..15cc7387bb6
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts
@@ -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;
+
+ let sut: AccessPolicySelectorService;
+
+ beforeEach(() => {
+ organizationService = mock();
+
+ 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 = {}) =>
+ Object.assign(
+ new Organization(),
+ {
+ id: "myOrgId",
+ enabled: true,
+ type: OrganizationUserType.Admin,
+ },
+ props
+ );
+
+function createApItemValueType(options: Partial = {}) {
+ 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;
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.ts
new file mode 100644
index 00000000000..3193f35729f
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.ts
@@ -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 {
+ 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;
+ }
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts
new file mode 100644
index 00000000000..55c14dacefe
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts
@@ -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;
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts
new file mode 100644
index 00000000000..2192e631bd5
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts
@@ -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,
+ };
+ });
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-item.enum.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-item.enum.ts
new file mode 100644
index 00000000000..6d060ac255d
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-item.enum.ts
@@ -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";
+ }
+ }
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-permission.enum.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-permission.enum.ts
new file mode 100644
index 00000000000..eb442b0af5d
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/enums/ap-permission.enum.ts
@@ -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;
+ }
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts
index c9b51b382ad..6a4cd92808d 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts
@@ -14,6 +14,7 @@ import {
GroupProjectAccessPolicyView,
GroupServiceAccountAccessPolicyView,
ProjectAccessPoliciesView,
+ ProjectPeopleAccessPoliciesView,
ServiceAccountAccessPoliciesView,
ServiceAccountProjectAccessPolicyView,
UserProjectAccessPolicyView,
@@ -21,6 +22,7 @@ import {
} from "../../models/view/access-policy.view";
import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
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 { ServiceAccountAccessPoliciesResponse } from "../../shared/access-policies/models/responses/service-accounts-access-policies.response";
@@ -36,6 +38,7 @@ import {
UserProjectAccessPolicyResponse,
} from "./models/responses/access-policy.response";
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
+import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response";
@Injectable({
providedIn: "root",
@@ -133,6 +136,37 @@ export class AccessPolicyService {
return await this.createProjectAccessPoliciesView(organizationId, results);
}
+ async getProjectPeopleAccessPolicies(
+ projectId: string
+ ): Promise {
+ 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(
serviceAccountId: string
): Promise {
@@ -258,6 +292,20 @@ export class AccessPolicyService {
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(
projectAccessPoliciesView: ProjectAccessPoliciesView
): AccessPoliciesCreateRequest {
@@ -288,6 +336,30 @@ export class AccessPolicyService {
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(
response: UserProjectAccessPolicyResponse
): UserProjectAccessPolicyView {
@@ -297,6 +369,7 @@ export class AccessPolicyService {
organizationUserId: response.organizationUserId,
organizationUserName: response.organizationUserName,
userId: response.userId,
+ currentUser: response.currentUser,
};
}
@@ -478,6 +551,8 @@ export class AccessPolicyService {
view.id = r.id;
view.type = r.type;
view.email = r.email;
+ view.currentUser = r.currentUser;
+ view.currentUserInGroup = r.currentUserInGroup;
if (r.type === "serviceAccount" || r.type === "project") {
view.name = await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey);
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/people-access-policies.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/people-access-policies.request.ts
new file mode 100644
index 00000000000..d7914ef4396
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/people-access-policies.request.ts
@@ -0,0 +1,6 @@
+import { AccessPolicyRequest } from "./access-policy.request";
+
+export class PeopleAccessPoliciesRequest {
+ userAccessPolicyRequests?: AccessPolicyRequest[];
+ groupAccessPolicyRequests?: AccessPolicyRequest[];
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts
index d17b22dc860..9aaa57a722a 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/access-policy.response.ts
@@ -22,6 +22,7 @@ export class UserProjectAccessPolicyResponse extends BaseAccessPolicyResponse {
organizationUserName: string;
grantedProjectId: string;
userId: string;
+ currentUser: boolean;
constructor(response: any) {
super(response);
@@ -29,6 +30,7 @@ export class UserProjectAccessPolicyResponse extends BaseAccessPolicyResponse {
this.organizationUserName = this.getResponseProperty("OrganizationUserName");
this.grantedProjectId = this.getResponseProperty("GrantedProjectId");
this.userId = this.getResponseProperty("UserId");
+ this.currentUser = this.getResponseProperty("CurrentUser");
}
}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/potential-grantee.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/potential-grantee.response.ts
index 4c8c3eb36e5..3d4572aaae3 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/potential-grantee.response.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/potential-grantee.response.ts
@@ -5,6 +5,8 @@ export class PotentialGranteeResponse extends BaseResponse {
name: string;
type: string;
email: string;
+ currentUserInGroup: boolean;
+ currentUser: boolean;
constructor(response: any) {
super(response);
@@ -12,5 +14,7 @@ export class PotentialGranteeResponse extends BaseResponse {
this.name = this.getResponseProperty("Name");
this.type = this.getResponseProperty("Type");
this.email = this.getResponseProperty("Email");
+ this.currentUserInGroup = this.getResponseProperty("CurrentUserInGroup");
+ this.currentUser = this.getResponseProperty("CurrentUser");
}
}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-people-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-people-access-policies.response.ts
new file mode 100644
index 00000000000..fb5ac17d061
--- /dev/null
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-people-access-policies.response.ts
@@ -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)
+ );
+ }
+}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts
index d2990f4c67f..f9b8eed298b 100644
--- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts
+++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts
@@ -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 { 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 { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-removal-dialog.component";
import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component";
@@ -37,6 +38,7 @@ import { SecretsListComponent } from "./secrets-list.component";
NoItemsModule,
AccessRemovalDialogComponent,
AccessSelectorComponent,
+ AccessPolicySelectorComponent,
BulkStatusDialogComponent,
BulkConfirmationDialogComponent,
HeaderComponent,
@@ -57,6 +59,7 @@ import { SecretsListComponent } from "./secrets-list.component";
SecretsListComponent,
AccessSelectorComponent,
OrgSuspendedComponent,
+ AccessPolicySelectorComponent,
],
providers: [],
bootstrap: [],