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 }} -

- - -
+
+
+

+ {{ "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 }} + + + + + + + + + {{ 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: [],