From 51c5e053f7f1c1112e0bce4e15b76304c3ad00f3 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:33:45 -0600 Subject: [PATCH] [SM-909] Migrate service account people tab to new selector (#6534) * migrate sa -> people tab to new selector * remove unused code * Add access token still available warning --- apps/web/src/locales/en/messages.json | 5 +- .../models/view/access-policy.view.ts | 3 +- .../service-account-people.component.html | 38 +- .../service-account-people.component.ts | 326 ++++++++++-------- .../service-accounts.component.ts | 3 - .../access-policy-selector.service.spec.ts | 118 +++++++ .../access-policy-selector.service.ts | 22 ++ .../models/ap-item-value.type.ts | 33 ++ .../models/ap-item-view.type.ts | 7 +- .../access-policies/access-policy.service.ts | 183 +++------- .../access-removal-dialog.component.html | 14 - .../access-removal-dialog.component.ts | 69 ---- .../responses/access-policy.response.ts | 2 + ...ccount-people-access-policies.response.ts} | 2 +- .../shared/sm-shared.module.ts | 3 - 15 files changed, 452 insertions(+), 376 deletions(-) delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.html delete mode 100644 bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts rename bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/{service-accounts-access-policies.response.ts => service-account-people-access-policies.response.ts} (91%) diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 0929019670..236ba76cfe 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -7415,7 +7415,7 @@ "example": "Unique ID" } } - }, + }, "seeDetailedInstructions": { "message": "See detailed instructions on our help site at", "description": "This is followed a by a hyperlink to the help website." @@ -7437,5 +7437,8 @@ }, "collectionAccessRestricted": { "message": "Collection access is restricted" + }, + "serviceAccountAccessUpdated": { + "message": "Service account 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 1b102acaa5..958fe0d48e 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 @@ -19,6 +19,7 @@ export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView { organizationUserName: string; grantedServiceAccountId: string; userId: string; + currentUser: boolean; } export class GroupProjectAccessPolicyView extends BaseAccessPolicyView { @@ -53,7 +54,7 @@ export class ProjectPeopleAccessPoliciesView { groupAccessPolicies: GroupProjectAccessPolicyView[]; } -export class ServiceAccountAccessPoliciesView { +export class ServiceAccountPeopleAccessPoliciesView { userAccessPolicies: UserServiceAccountAccessPolicyView[]; groupAccessPolicies: GroupServiceAccountAccessPolicyView[]; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html index 2a490d7914..79c8132bbc 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.html @@ -1,16 +1,22 @@ -
-

- {{ "serviceAccountPeopleDescription" | i18n }} -

- - -
+
+
+

+ {{ "serviceAccountPeopleDescription" | i18n }} +

+ + + +
+
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts index 4874605040..04e0617815 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/people/service-account-people.component.ts @@ -1,162 +1,93 @@ -import { Component } from "@angular/core"; -import { ActivatedRoute } from "@angular/router"; -import { - combineLatestWith, - 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 { catchError, combineLatest, EMPTY, Subject, switchMap, takeUntil } 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, SimpleDialogOptions } from "@bitwarden/components"; -import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; +import { DialogService } from "@bitwarden/components"; +import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service"; import { - GroupServiceAccountAccessPolicyView, - ServiceAccountAccessPoliciesView, - UserServiceAccountAccessPolicyView, -} from "../../models/view/access-policy.view"; + ApItemValueType, + convertToServiceAccountPeopleAccessPoliciesView, +} 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 { ApPermissionEnum } from "../../shared/access-policies/access-policy-selector/models/enums/ap-permission.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-service-account-people", templateUrl: "./service-account-people.component.html", }) -export class ServiceAccountPeopleComponent { +export class ServiceAccountPeopleComponent implements OnInit, OnDestroy { + private currentAccessPolicies: ApItemViewType[]; private destroy$ = new Subject(); - private serviceAccountId: string; private organizationId: string; - private rows: AccessSelectorRowView[]; + private serviceAccountId: string; - protected rows$: Observable = - this.accessPolicyService.serviceAccountAccessPolicyChanges$.pipe( - startWith(null), - combineLatestWith(this.route.params), - switchMap(([_, params]) => - this.accessPolicyService.getServiceAccountAccessPolicies(params.serviceAccountId), - ), - 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, - static: true, - }); - }); + private currentAccessPolicies$ = combineLatest([this.route.params]).pipe( + switchMap(([params]) => + this.accessPolicyService + .getServiceAccountPeopleAccessPolicies(params.serviceAccountId) + .then((policies) => { + return convertToAccessPolicyItemViews(policies); + }), + ), + catchError(() => { + this.router.navigate(["/sm", this.organizationId, "service-accounts"]); + 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, - static: true, - }); - }); + private potentialGrantees$ = combineLatest([this.route.params]).pipe( + switchMap(([params]) => + this.accessPolicyService + .getPeoplePotentialGrantees(params.organizationId) + .then((grantees) => { + return convertPotentialGranteesToApItemViewType(grantees); + }), + ), + ); - return rows; - }), - share(), - ); + protected formGroup = new FormGroup({ + accessPolicies: new FormControl([] as ApItemValueType[]), + }); - protected handleCreateAccessPolicies(selected: SelectItemView[]) { - const serviceAccountAccessPoliciesView = new ServiceAccountAccessPoliciesView(); - serviceAccountAccessPoliciesView.userAccessPolicies = selected - .filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "user") - .map((filtered) => { - const view = new UserServiceAccountAccessPolicyView(); - view.grantedServiceAccountId = this.serviceAccountId; - view.organizationUserId = filtered.id; - view.read = true; - view.write = true; - return view; - }); - - serviceAccountAccessPoliciesView.groupAccessPolicies = selected - .filter((selection) => AccessSelectorComponent.getAccessItemType(selection) === "group") - .map((filtered) => { - const view = new GroupServiceAccountAccessPolicyView(); - view.grantedServiceAccountId = this.serviceAccountId; - view.groupId = filtered.id; - view.read = true; - view.write = true; - return view; - }); - - return this.accessPolicyService.createServiceAccountAccessPolicies( - this.serviceAccountId, - serviceAccountAccessPoliciesView, - ); - } - - 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); - const simpleDialogOpts: SimpleDialogOptions = { - title: this.i18nService.t("saPeopleWarningTitle"), - content: this.i18nService.t("saPeopleWarningMessage"), - type: "warning", - acceptButtonText: { key: "close" }, - cancelButtonText: null, - }; - this.dialogService.openSimpleDialogRef(simpleDialogOpts); - } catch (e) { - this.validationService.showError(e); - } - } + protected loading = true; + protected potentialGrantees: ApItemViewType[]; + protected staticPermission = ApPermissionEnum.CanReadWrite; constructor( private route: ActivatedRoute, private dialogService: DialogService, - private i18nService: I18nService, + private changeDetectorRef: ChangeDetectorRef, private validationService: ValidationService, private accessPolicyService: AccessPolicyService, + private router: Router, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private accessPolicySelectorService: AccessPolicySelectorService, ) {} ngOnInit(): void { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { - this.serviceAccountId = params.serviceAccountId; this.organizationId = params.organizationId; + this.serviceAccountId = params.serviceAccountId; }); - 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,16 +95,133 @@ export class ServiceAccountPeopleComponent { this.destroy$.complete(); } - private launchDeleteWarningDialog(policy: AccessSelectorRowView) { - this.dialogService.open(AccessRemovalDialogComponent, { - data: { - title: "smAccessRemovalWarningSaTitle", - message: "smAccessRemovalWarningSaMessage", - operation: "delete", - type: "service-account", - returnRoute: ["sm", this.organizationId, "service-accounts"], - policy, - }, + submit = async () => { + if (this.isFormInvalid()) { + return; + } + + const showAccessRemovalWarning = + await this.accessPolicySelectorService.showAccessRemovalWarning( + this.organizationId, + this.formGroup.value.accessPolicies, + ); + + if ( + await this.handleAccessRemovalWarning(showAccessRemovalWarning, this.currentAccessPolicies) + ) { + return; + } + + try { + const peoplePoliciesViews = await this.updateServiceAccountPeopleAccessPolicies( + this.serviceAccountId, + this.formGroup.value.accessPolicies, + ); + + await this.handleAccessTokenAvailableWarning( + showAccessRemovalWarning, + this.currentAccessPolicies, + this.formGroup.value.accessPolicies, + ); + + this.currentAccessPolicies = convertToAccessPolicyItemViews(peoplePoliciesViews); + + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("serviceAccountAccessUpdated"), + ); + } 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 isFormInvalid(): boolean { + this.formGroup.markAllAsTouched(); + return this.formGroup.invalid; + } + + private async handleAccessRemovalWarning( + showAccessRemovalWarning: boolean, + currentAccessPolicies: ApItemViewType[], + ): Promise { + if (showAccessRemovalWarning) { + const confirmed = await this.showWarning(); + if (!confirmed) { + this.setSelected(currentAccessPolicies); + return true; + } + } + return false; + } + + private async updateServiceAccountPeopleAccessPolicies( + serviceAccountId: string, + selectedPolicies: ApItemValueType[], + ) { + const serviceAccountPeopleView = convertToServiceAccountPeopleAccessPoliciesView( + serviceAccountId, + selectedPolicies, + ); + return await this.accessPolicyService.putServiceAccountPeopleAccessPolicies( + serviceAccountId, + serviceAccountPeopleView, + ); + } + + private async handleAccessTokenAvailableWarning( + showAccessRemovalWarning: boolean, + currentAccessPolicies: ApItemViewType[], + selectedPolicies: ApItemValueType[], + ): Promise { + if (showAccessRemovalWarning) { + this.router.navigate(["sm", this.organizationId, "service-accounts"]); + } else if ( + this.accessPolicySelectorService.isAccessRemoval(currentAccessPolicies, selectedPolicies) + ) { + await this.showAccessTokenStillAvailableWarning(); + } + } + + private async showWarning(): Promise { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "smAccessRemovalWarningSaTitle" }, + content: { key: "smAccessRemovalWarningSaMessage" }, + acceptButtonText: { key: "removeAccess" }, + cancelButtonText: { key: "cancel" }, + type: "warning", + }); + return confirmed; + } + + private async showAccessTokenStillAvailableWarning(): Promise { + await this.dialogService.openSimpleDialog({ + title: { key: "saPeopleWarningTitle" }, + content: { key: "saPeopleWarningMessage" }, + type: "warning", + acceptButtonText: { key: "close" }, + cancelButtonText: null, }); } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts index 70d4e0c9c3..a5e3cd29d2 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/service-accounts/service-accounts.component.ts @@ -9,7 +9,6 @@ import { ServiceAccountSecretsDetailsView, ServiceAccountView, } from "../models/view/service-account.view"; -import { AccessPolicyService } from "../shared/access-policies/access-policy.service"; import { ServiceAccountDeleteDialogComponent, @@ -36,7 +35,6 @@ export class ServiceAccountsComponent implements OnInit { constructor( private route: ActivatedRoute, private dialogService: DialogService, - private accessPolicyService: AccessPolicyService, private serviceAccountService: ServiceAccountService, private organizationService: OrganizationService, ) {} @@ -45,7 +43,6 @@ export class ServiceAccountsComponent implements OnInit { this.serviceAccounts$ = combineLatest([ this.route.params, this.serviceAccountService.serviceAccount$.pipe(startWith(null)), - this.accessPolicyService.serviceAccountAccessPolicyChanges$.pipe(startWith(null)), ]).pipe( switchMap(async ([params]) => { this.organizationId = params.organizationId; 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 index 9f200f2f56..482d2bb06b 100644 --- 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 @@ -6,6 +6,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccessPolicySelectorService } from "./access-policy-selector.service"; import { ApItemValueType } from "./models/ap-item-value.type"; +import { ApItemViewType } from "./models/ap-item-view.type"; import { ApItemEnum } from "./models/enums/ap-item.enum"; import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; @@ -207,6 +208,113 @@ describe("AccessPolicySelectorService", () => { const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); + expect(result).toBe(true); + }); + }); + describe("isAccessRemoval", () => { + it("returns false when there are no previous policies and no selected policies", async () => { + const currentAccessPolicies: ApItemViewType[] = []; + const selectedPolicyValues: ApItemValueType[] = []; + + const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); + + expect(result).toBe(false); + }); + it("returns false when there are no previous policies", async () => { + const currentAccessPolicies: ApItemViewType[] = []; + const selectedPolicyValues: ApItemValueType[] = [ + createApItemValueType({ + id: "example", + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + ]; + + const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); + + expect(result).toBe(false); + }); + it("returns false when previous policies and selected policies are the same", async () => { + const currentAccessPolicies: ApItemViewType[] = [ + createApItemViewType({ + id: "example", + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + ]; + const selectedPolicyValues: ApItemValueType[] = [ + createApItemValueType({ + id: "example", + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + ]; + + const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); + + expect(result).toBe(false); + }); + it("returns false when previous policies are still selected", async () => { + const currentAccessPolicies: ApItemViewType[] = [ + createApItemViewType({ + id: "example", + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + ]; + const selectedPolicyValues: ApItemValueType[] = [ + createApItemValueType({ + id: "example", + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + createApItemValueType({ + id: "example-2", + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + ]; + + const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); + + expect(result).toBe(false); + }); + it("returns true when previous policies are not selected", async () => { + const currentAccessPolicies: ApItemViewType[] = [ + createApItemViewType({ + id: "example", + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + ]; + const selectedPolicyValues: ApItemValueType[] = [ + createApItemValueType({ + id: "test", + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + createApItemValueType({ + id: "example-2", + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + ]; + + const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); + + expect(result).toBe(true); + }); + it("returns true when there are previous policies and nothing was selected", async () => { + const currentAccessPolicies: ApItemViewType[] = [ + createApItemViewType({ + permission: ApPermissionEnum.CanRead, + currentUser: true, + }), + ]; + const selectedPolicyValues: ApItemValueType[] = []; + + const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); + expect(result).toBe(true); }); }); @@ -232,6 +340,16 @@ function createApItemValueType(options: Partial = {}) { }; } +function createApItemViewType(options: Partial = {}) { + return { + id: options?.id ?? "test", + listName: options?.listName ?? "test", + labelName: options?.labelName ?? "test", + type: options?.type ?? ApItemEnum.User, + permission: options?.permission ?? ApPermissionEnum.CanRead, + }; +} + function setupUserOrg() { const userId = "testUserId"; const org = orgFactory({ userId: userId }); 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 index 7923590ab4..4a90172d45 100644 --- 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 @@ -3,6 +3,7 @@ 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 { ApItemViewType } from "./models/ap-item-view.type"; import { ApItemEnum } from "./models/enums/ap-item.enum"; import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; @@ -45,4 +46,25 @@ export class AccessPolicySelectorService { return false; } + + isAccessRemoval(current: ApItemViewType[], selected: ApItemValueType[]): boolean { + if (current?.length === 0) { + return false; + } + + if (selected?.length === 0) { + return true; + } + + return this.isAnyCurrentIdNotInSelectedIds(current, selected); + } + + private isAnyCurrentIdNotInSelectedIds( + current: ApItemViewType[], + selected: ApItemValueType[], + ): boolean { + const currentIds = current.map((x) => x.id); + const selectedIds = selected.map((x) => x.id); + return !currentIds.every((id) => selectedIds.includes(id)); + } } 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 index 2e6180a97e..362f3c524a 100644 --- 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 @@ -2,6 +2,9 @@ import { ProjectPeopleAccessPoliciesView, UserProjectAccessPolicyView, GroupProjectAccessPolicyView, + ServiceAccountPeopleAccessPoliciesView, + UserServiceAccountAccessPolicyView, + GroupServiceAccountAccessPolicyView, } from "../../../../models/view/access-policy.view"; import { ApItemEnum } from "./enums/ap-item.enum"; @@ -43,3 +46,33 @@ export function convertToProjectPeopleAccessPoliciesView( }); return view; } + +export function convertToServiceAccountPeopleAccessPoliciesView( + serviceAccountId: string, + selectedPolicyValues: ApItemValueType[], +): ServiceAccountPeopleAccessPoliciesView { + const view = new ServiceAccountPeopleAccessPoliciesView(); + view.userAccessPolicies = selectedPolicyValues + .filter((x) => x.type == ApItemEnum.User) + .map((filtered) => { + const policyView = new UserServiceAccountAccessPolicyView(); + policyView.grantedServiceAccountId = serviceAccountId; + policyView.organizationUserId = filtered.id; + policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); + policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); + policyView.currentUser = filtered.currentUser; + return policyView; + }); + + view.groupAccessPolicies = selectedPolicyValues + .filter((x) => x.type == ApItemEnum.Group) + .map((filtered) => { + const policyView = new GroupServiceAccountAccessPolicyView(); + policyView.grantedServiceAccountId = serviceAccountId; + 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 index e0d87c0d85..1f494b8fbf 100644 --- 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 @@ -1,7 +1,10 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SelectItemView } from "@bitwarden/components"; -import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policy.view"; +import { + ProjectPeopleAccessPoliciesView, + ServiceAccountPeopleAccessPoliciesView, +} from "../../../../models/view/access-policy.view"; import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view"; import { ApItemEnum, ApItemEnumUtil } from "./enums/ap-item.enum"; @@ -29,7 +32,7 @@ export type ApItemViewType = SelectItemView & { ); export function convertToAccessPolicyItemViews( - value: ProjectPeopleAccessPoliciesView, + value: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView, ): ApItemViewType[] { const accessPolicies: ApItemViewType[] = []; 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 9fc424c4f4..05b95e127d 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 @@ -2,7 +2,6 @@ import { Injectable } from "@angular/core"; import { Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -15,18 +14,16 @@ import { GroupServiceAccountAccessPolicyView, ProjectAccessPoliciesView, ProjectPeopleAccessPoliciesView, - ServiceAccountAccessPoliciesView, ServiceAccountProjectAccessPolicyView, UserProjectAccessPolicyView, UserServiceAccountAccessPolicyView, + ServiceAccountPeopleAccessPoliciesView, } 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"; -import { AccessSelectorRowView } from "./access-selector.component"; import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request"; import { GrantedPolicyRequest } from "./models/requests/granted-policy.request"; @@ -39,13 +36,13 @@ import { } from "./models/responses/access-policy.response"; import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response"; +import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response"; @Injectable({ providedIn: "root", }) export class AccessPolicyService { private _projectAccessPolicyChanges$ = new Subject(); - private _serviceAccountAccessPolicyChanges$ = new Subject(); private _serviceAccountGrantedPolicyChanges$ = new Subject< ServiceAccountProjectAccessPolicyView[] >(); @@ -55,12 +52,6 @@ export class AccessPolicyService { */ readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable(); - /** - * Emits when a service account access policy is created or deleted. - */ - readonly serviceAccountAccessPolicyChanges$ = - this._serviceAccountAccessPolicyChanges$.asObservable(); - /** * Emits when a service account granted policy is created or deleted. */ @@ -69,7 +60,6 @@ export class AccessPolicyService { constructor( private cryptoService: CryptoService, - private organizationService: OrganizationService, protected apiService: ApiService, protected encryptService: EncryptService, ) {} @@ -78,10 +68,6 @@ export class AccessPolicyService { this._projectAccessPolicyChanges$.next(null); } - refreshServiceAccountAccessPolicyChanges() { - this._serviceAccountAccessPolicyChanges$.next(null); - } - async getGrantedPolicies( serviceAccountId: string, organizationId: string, @@ -167,19 +153,35 @@ export class AccessPolicyService { return this.createProjectPeopleAccessPoliciesView(results); } - async getServiceAccountAccessPolicies( + async getServiceAccountPeopleAccessPolicies( serviceAccountId: string, - ): Promise { + ): Promise { const r = await this.apiService.send( "GET", - "/service-accounts/" + serviceAccountId + "/access-policies", + "/service-accounts/" + serviceAccountId + "/access-policies/people", null, true, true, ); - const results = new ServiceAccountAccessPoliciesResponse(r); - return await this.createServiceAccountAccessPoliciesView(results); + const results = new ServiceAccountPeopleAccessPoliciesResponse(r); + return this.createServiceAccountPeopleAccessPoliciesView(results); + } + + async putServiceAccountPeopleAccessPolicies( + serviceAccountId: string, + peoplePoliciesView: ServiceAccountPeopleAccessPoliciesView, + ) { + const request = this.getPeopleAccessPoliciesRequest(peoplePoliciesView); + const r = await this.apiService.send( + "PUT", + "/service-accounts/" + serviceAccountId + "/access-policies/people", + request, + true, + true, + ); + const results = new ServiceAccountPeopleAccessPoliciesResponse(r); + return this.createServiceAccountPeopleAccessPoliciesView(results); } async createProjectAccessPolicies( @@ -201,30 +203,9 @@ export class AccessPolicyService { return view; } - async createServiceAccountAccessPolicies( - serviceAccountId: string, - serviceAccountAccessPoliciesView: ServiceAccountAccessPoliciesView, - ): Promise { - const request = this.getServiceAccountAccessPoliciesCreateRequest( - serviceAccountAccessPoliciesView, - ); - const r = await this.apiService.send( - "POST", - "/service-accounts/" + serviceAccountId + "/access-policies", - request, - true, - true, - ); - const results = new ServiceAccountAccessPoliciesResponse(r); - const view = await this.createServiceAccountAccessPoliciesView(results); - this._serviceAccountAccessPolicyChanges$.next(view); - return view; - } - async deleteAccessPolicy(accessPolicyId: string): Promise { await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false); this._projectAccessPolicyChanges$.next(null); - this._serviceAccountAccessPolicyChanges$.next(null); this._serviceAccountGrantedPolicyChanges$.next(null); } @@ -241,36 +222,6 @@ export class AccessPolicyService { ); } - async needToShowAccessRemovalWarning( - organizationId: string, - policy: AccessSelectorRowView, - currentPolicies: AccessSelectorRowView[], - ): Promise { - const organization = this.organizationService.get(organizationId); - if (organization.isOwner || organization.isAdmin) { - return false; - } - const currentUserId = organization.userId; - const readWriteGroupPolicies = currentPolicies - .filter((x) => x.accessPolicyId != policy.accessPolicyId) - .filter((x) => x.currentUserInGroup && x.read && x.write).length; - const readWriteUserPolicies = currentPolicies - .filter((x) => x.accessPolicyId != policy.accessPolicyId) - .filter((x) => x.userId == currentUserId && x.read && x.write).length; - - if (policy.type === "user" && policy.userId == currentUserId && readWriteGroupPolicies == 0) { - return true; - } else if ( - policy.type === "group" && - policy.currentUserInGroup && - readWriteUserPolicies == 0 && - readWriteGroupPolicies == 0 - ) { - return true; - } - return false; - } - private async createProjectAccessPoliciesView( organizationId: string, projectAccessPoliciesResponse: ProjectAccessPoliciesResponse, @@ -306,6 +257,20 @@ export class AccessPolicyService { return view; } + private createServiceAccountPeopleAccessPoliciesView( + response: ServiceAccountPeopleAccessPoliciesResponse, + ): ServiceAccountPeopleAccessPoliciesView { + const view = new ServiceAccountPeopleAccessPoliciesView(); + + view.userAccessPolicies = response.userAccessPolicies.map((ap) => { + return this.createUserServiceAccountAccessPolicyView(ap); + }); + view.groupAccessPolicies = response.groupAccessPolicies.map((ap) => { + return this.createGroupServiceAccountAccessPolicyView(ap); + }); + return view; + } + private getAccessPoliciesCreateRequest( projectAccessPoliciesView: ProjectAccessPoliciesView, ): AccessPoliciesCreateRequest { @@ -337,24 +302,20 @@ export class AccessPolicyService { } private getPeopleAccessPoliciesRequest( - projectPeopleAccessPoliciesView: ProjectPeopleAccessPoliciesView, + view: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView, ): PeopleAccessPoliciesRequest { const request = new PeopleAccessPoliciesRequest(); - if (projectPeopleAccessPoliciesView.userAccessPolicies?.length > 0) { - request.userAccessPolicyRequests = projectPeopleAccessPoliciesView.userAccessPolicies.map( - (ap) => { - return this.getAccessPolicyRequest(ap.organizationUserId, ap); - }, - ); + if (view.userAccessPolicies?.length > 0) { + request.userAccessPolicyRequests = view.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); - }, - ); + if (view.groupAccessPolicies?.length > 0) { + request.groupAccessPolicyRequests = view.groupAccessPolicies.map((ap) => { + return this.getAccessPolicyRequest(ap.groupId, ap); + }); } return request; @@ -399,50 +360,15 @@ export class AccessPolicyService { organizationKey, ) : null, - serviceAccountName: await this.encryptService.decryptToUtf8( - new EncString(response.serviceAccountName), - organizationKey, - ), + serviceAccountName: response.serviceAccountName + ? await this.encryptService.decryptToUtf8( + new EncString(response.serviceAccountName), + organizationKey, + ) + : null, }; } - private getServiceAccountAccessPoliciesCreateRequest( - serviceAccountAccessPoliciesView: ServiceAccountAccessPoliciesView, - ): AccessPoliciesCreateRequest { - const createRequest = new AccessPoliciesCreateRequest(); - - if (serviceAccountAccessPoliciesView.userAccessPolicies?.length > 0) { - createRequest.userAccessPolicyRequests = - serviceAccountAccessPoliciesView.userAccessPolicies.map((ap) => { - return this.getAccessPolicyRequest(ap.organizationUserId, ap); - }); - } - - if (serviceAccountAccessPoliciesView.groupAccessPolicies?.length > 0) { - createRequest.groupAccessPolicyRequests = - serviceAccountAccessPoliciesView.groupAccessPolicies.map((ap) => { - return this.getAccessPolicyRequest(ap.groupId, ap); - }); - } - - return createRequest; - } - - private async createServiceAccountAccessPoliciesView( - serviceAccountAccessPoliciesResponse: ServiceAccountAccessPoliciesResponse, - ): Promise { - const view = new ServiceAccountAccessPoliciesView(); - view.userAccessPolicies = serviceAccountAccessPoliciesResponse.userAccessPolicies.map((ap) => { - return this.createUserServiceAccountAccessPolicyView(ap); - }); - view.groupAccessPolicies = serviceAccountAccessPoliciesResponse.groupAccessPolicies.map( - (ap) => { - return this.createGroupServiceAccountAccessPolicyView(ap); - }, - ); - return view; - } - private createUserServiceAccountAccessPolicyView( response: UserServiceAccountAccessPolicyResponse, ): UserServiceAccountAccessPolicyView { @@ -452,6 +378,7 @@ export class AccessPolicyService { organizationUserId: response.organizationUserId, organizationUserName: response.organizationUserName, userId: response.userId, + currentUser: response.currentUser, }; } @@ -555,7 +482,9 @@ export class AccessPolicyService { view.currentUserInGroup = r.currentUserInGroup; if (r.type === "serviceAccount" || r.type === "project") { - view.name = await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey); + view.name = r.name + ? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey) + : null; } else { view.name = r.name; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.html deleted file mode 100644 index 66ca0ce72f..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.html +++ /dev/null @@ -1,14 +0,0 @@ - - {{ data.title | i18n }} - - {{ data.message | i18n }} - - - - - - diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts deleted file mode 100644 index d6464421f1..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/dialogs/access-removal-dialog.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; -import { Component, Inject, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; - -import { AccessPolicyService } from "../access-policy.service"; -import { AccessSelectorComponent, AccessSelectorRowView } from "../access-selector.component"; - -export interface AccessRemovalDetails { - title: string; - message: string; - operation: "update" | "delete"; - type: "project" | "service-account"; - returnRoute: string[]; - policy: AccessSelectorRowView; -} - -@Component({ - templateUrl: "./access-removal-dialog.component.html", -}) -export class AccessRemovalDialogComponent implements OnInit { - constructor( - public dialogRef: DialogRef, - private router: Router, - private accessPolicyService: AccessPolicyService, - @Inject(DIALOG_DATA) public data: AccessRemovalDetails, - ) {} - - ngOnInit(): void { - // TODO remove null checks once strictNullChecks in TypeScript is turned on. - if ( - !this.data.message || - !this.data.title || - !this.data.operation || - !this.data.returnRoute || - !this.data.policy - ) { - this.dialogRef.close(); - throw new Error( - "The access removal dialog was not called with the appropriate operation values.", - ); - } - } - - removeAccess = async () => { - await this.router.navigate(this.data.returnRoute); - if (this.data.operation === "delete") { - await this.accessPolicyService.deleteAccessPolicy(this.data.policy.accessPolicyId); - } else if (this.data.operation == "update") { - await this.accessPolicyService.updateAccessPolicy( - AccessSelectorComponent.getBaseAccessPolicyView(this.data.policy), - ); - this.refreshPolicyChanges(); - } - this.dialogRef.close(); - }; - - cancel = () => { - this.refreshPolicyChanges(); - this.dialogRef.close(); - }; - - private refreshPolicyChanges() { - if (this.data.type == "project") { - this.accessPolicyService.refreshProjectAccessPolicyChanges(); - } else if (this.data.type == "service-account") { - this.accessPolicyService.refreshServiceAccountAccessPolicyChanges(); - } - } -} 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 9aaa57a722..ef076f9b59 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 @@ -39,6 +39,7 @@ export class UserServiceAccountAccessPolicyResponse extends BaseAccessPolicyResp organizationUserName: string; grantedServiceAccountId: string; userId: string; + currentUser: boolean; constructor(response: any) { super(response); @@ -46,6 +47,7 @@ export class UserServiceAccountAccessPolicyResponse extends BaseAccessPolicyResp this.organizationUserName = this.getResponseProperty("OrganizationUserName"); this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId"); 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/service-accounts-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-people-access-policies.response.ts similarity index 91% rename from bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-accounts-access-policies.response.ts rename to bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-people-access-policies.response.ts index a4d1cfc4c3..ca134d9012 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-accounts-access-policies.response.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-people-access-policies.response.ts @@ -5,7 +5,7 @@ import { UserServiceAccountAccessPolicyResponse, } from "./access-policy.response"; -export class ServiceAccountAccessPoliciesResponse extends BaseResponse { +export class ServiceAccountPeopleAccessPoliciesResponse extends BaseResponse { userAccessPolicies: UserServiceAccountAccessPolicyResponse[]; groupAccessPolicies: GroupServiceAccountAccessPolicyResponse[]; 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 f9b8eed298..cf312f4a2f 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 @@ -13,7 +13,6 @@ 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"; import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component"; import { HeaderComponent } from "./header.component"; @@ -36,7 +35,6 @@ import { SecretsListComponent } from "./secrets-list.component"; exports: [ SharedModule, NoItemsModule, - AccessRemovalDialogComponent, AccessSelectorComponent, AccessPolicySelectorComponent, BulkStatusDialogComponent, @@ -50,7 +48,6 @@ import { SecretsListComponent } from "./secrets-list.component"; SharedModule, ], declarations: [ - AccessRemovalDialogComponent, BulkStatusDialogComponent, BulkConfirmationDialogComponent, HeaderComponent,