[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
This commit is contained in:
Thomas Avery 2023-12-07 15:33:45 -06:00 committed by GitHub
parent e5b8fd4388
commit 51c5e053f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 452 additions and 376 deletions

View File

@ -7415,7 +7415,7 @@
"example": "Unique ID" "example": "Unique ID"
} }
} }
}, },
"seeDetailedInstructions": { "seeDetailedInstructions": {
"message": "See detailed instructions on our help site at", "message": "See detailed instructions on our help site at",
"description": "This is followed a by a hyperlink to the help website." "description": "This is followed a by a hyperlink to the help website."
@ -7437,5 +7437,8 @@
}, },
"collectionAccessRestricted": { "collectionAccessRestricted": {
"message": "Collection access is restricted" "message": "Collection access is restricted"
},
"serviceAccountAccessUpdated": {
"message": "Service account access updated"
} }
} }

View File

@ -19,6 +19,7 @@ export class UserServiceAccountAccessPolicyView extends BaseAccessPolicyView {
organizationUserName: string; organizationUserName: string;
grantedServiceAccountId: string; grantedServiceAccountId: string;
userId: string; userId: string;
currentUser: boolean;
} }
export class GroupProjectAccessPolicyView extends BaseAccessPolicyView { export class GroupProjectAccessPolicyView extends BaseAccessPolicyView {
@ -53,7 +54,7 @@ export class ProjectPeopleAccessPoliciesView {
groupAccessPolicies: GroupProjectAccessPolicyView[]; groupAccessPolicies: GroupProjectAccessPolicyView[];
} }
export class ServiceAccountAccessPoliciesView { export class ServiceAccountPeopleAccessPoliciesView {
userAccessPolicies: UserServiceAccountAccessPolicyView[]; userAccessPolicies: UserServiceAccountAccessPolicyView[];
groupAccessPolicies: GroupServiceAccountAccessPolicyView[]; groupAccessPolicies: GroupServiceAccountAccessPolicyView[];
} }

View File

@ -1,16 +1,22 @@
<div class="tw-mt-4 tw-w-2/5"> <form [formGroup]="formGroup" [bitSubmit]="submit">
<p class="tw-mt-6"> <div class="tw-w-2/5">
{{ "serviceAccountPeopleDescription" | i18n }} <p class="tw-mt-8" *ngIf="!loading">
</p> {{ "serviceAccountPeopleDescription" | i18n }}
<sm-access-selector </p>
[rows]="rows$ | async" <sm-access-policy-selector
granteeType="people" [loading]="loading"
[label]="'people' | i18n" formControlName="accessPolicies"
[hint]="'projectPeopleSelectHint' | i18n" [addButtonMode]="true"
[columnTitle]="'name' | i18n" [items]="potentialGrantees"
[emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n" [label]="'people' | i18n"
(onCreateAccessPolicies)="handleCreateAccessPolicies($event)" [hint]="'projectPeopleSelectHint' | i18n"
(onDeleteAccessPolicy)="handleDeleteAccessPolicy($event)" [columnTitle]="'name' | i18n"
> [emptyMessage]="'projectEmptyPeopleAccessPolicies' | i18n"
</sm-access-selector> [staticPermission]="staticPermission"
</div> >
</sm-access-policy-selector>
<button bitButton buttonType="primary" bitFormButton type="submit" class="tw-mt-7">
{{ "save" | i18n }}
</button>
</div>
</form>

View File

@ -1,162 +1,93 @@
import { Component } from "@angular/core"; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { FormControl, FormGroup } from "@angular/forms";
import { import { ActivatedRoute, Router } from "@angular/router";
combineLatestWith, import { catchError, combineLatest, EMPTY, Subject, switchMap, takeUntil } from "rxjs";
map,
Observable,
share,
startWith,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service";
import { import {
GroupServiceAccountAccessPolicyView, ApItemValueType,
ServiceAccountAccessPoliciesView, convertToServiceAccountPeopleAccessPoliciesView,
UserServiceAccountAccessPolicyView, } from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
} from "../../models/view/access-policy.view"; import {
ApItemViewType,
convertPotentialGranteesToApItemViewType,
convertToAccessPolicyItemViews,
} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
import { ApItemEnum } from "../../shared/access-policies/access-policy-selector/models/enums/ap-item.enum";
import { ApPermissionEnum } from "../../shared/access-policies/access-policy-selector/models/enums/ap-permission.enum";
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service"; import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
import {
AccessSelectorComponent,
AccessSelectorRowView,
} from "../../shared/access-policies/access-selector.component";
import {
AccessRemovalDetails,
AccessRemovalDialogComponent,
} from "../../shared/access-policies/dialogs/access-removal-dialog.component";
@Component({ @Component({
selector: "sm-service-account-people", selector: "sm-service-account-people",
templateUrl: "./service-account-people.component.html", templateUrl: "./service-account-people.component.html",
}) })
export class ServiceAccountPeopleComponent { export class ServiceAccountPeopleComponent implements OnInit, OnDestroy {
private currentAccessPolicies: ApItemViewType[];
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private serviceAccountId: string;
private organizationId: string; private organizationId: string;
private rows: AccessSelectorRowView[]; private serviceAccountId: string;
protected rows$: Observable<AccessSelectorRowView[]> = private currentAccessPolicies$ = combineLatest([this.route.params]).pipe(
this.accessPolicyService.serviceAccountAccessPolicyChanges$.pipe( switchMap(([params]) =>
startWith(null), this.accessPolicyService
combineLatestWith(this.route.params), .getServiceAccountPeopleAccessPolicies(params.serviceAccountId)
switchMap(([_, params]) => .then((policies) => {
this.accessPolicyService.getServiceAccountAccessPolicies(params.serviceAccountId), return convertToAccessPolicyItemViews(policies);
), }),
map((policies) => { ),
const rows: AccessSelectorRowView[] = []; catchError(() => {
policies.userAccessPolicies.forEach((policy) => { this.router.navigate(["/sm", this.organizationId, "service-accounts"]);
rows.push({ return EMPTY;
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,
});
});
policies.groupAccessPolicies.forEach((policy) => { private potentialGrantees$ = combineLatest([this.route.params]).pipe(
rows.push({ switchMap(([params]) =>
type: "group", this.accessPolicyService
name: policy.groupName, .getPeoplePotentialGrantees(params.organizationId)
id: policy.groupId, .then((grantees) => {
accessPolicyId: policy.id, return convertPotentialGranteesToApItemViewType(grantees);
read: policy.read, }),
write: policy.write, ),
currentUserInGroup: policy.currentUserInGroup, );
icon: AccessSelectorComponent.groupIcon,
static: true,
});
});
return rows; protected formGroup = new FormGroup({
}), accessPolicies: new FormControl([] as ApItemValueType[]),
share(), });
);
protected handleCreateAccessPolicies(selected: SelectItemView[]) { protected loading = true;
const serviceAccountAccessPoliciesView = new ServiceAccountAccessPoliciesView(); protected potentialGrantees: ApItemViewType[];
serviceAccountAccessPoliciesView.userAccessPolicies = selected protected staticPermission = ApPermissionEnum.CanReadWrite;
.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);
}
}
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private dialogService: DialogService, private dialogService: DialogService,
private i18nService: I18nService, private changeDetectorRef: ChangeDetectorRef,
private validationService: ValidationService, private validationService: ValidationService,
private accessPolicyService: AccessPolicyService, private accessPolicyService: AccessPolicyService,
private router: Router,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private accessPolicySelectorService: AccessPolicySelectorService,
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => { this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.serviceAccountId = params.serviceAccountId;
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
this.serviceAccountId = params.serviceAccountId;
}); });
this.rows$.pipe(takeUntil(this.destroy$)).subscribe((rows) => { combineLatest([this.potentialGrantees$, this.currentAccessPolicies$])
this.rows = rows; .pipe(takeUntil(this.destroy$))
}); .subscribe(([potentialGrantees, currentAccessPolicies]) => {
this.potentialGrantees = potentialGrantees;
this.setSelected(currentAccessPolicies);
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -164,16 +95,133 @@ export class ServiceAccountPeopleComponent {
this.destroy$.complete(); this.destroy$.complete();
} }
private launchDeleteWarningDialog(policy: AccessSelectorRowView) { submit = async () => {
this.dialogService.open<unknown, AccessRemovalDetails>(AccessRemovalDialogComponent, { if (this.isFormInvalid()) {
data: { return;
title: "smAccessRemovalWarningSaTitle", }
message: "smAccessRemovalWarningSaMessage",
operation: "delete", const showAccessRemovalWarning =
type: "service-account", await this.accessPolicySelectorService.showAccessRemovalWarning(
returnRoute: ["sm", this.organizationId, "service-accounts"], this.organizationId,
policy, 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<boolean> {
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<void> {
if (showAccessRemovalWarning) {
this.router.navigate(["sm", this.organizationId, "service-accounts"]);
} else if (
this.accessPolicySelectorService.isAccessRemoval(currentAccessPolicies, selectedPolicies)
) {
await this.showAccessTokenStillAvailableWarning();
}
}
private async showWarning(): Promise<boolean> {
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<void> {
await this.dialogService.openSimpleDialog({
title: { key: "saPeopleWarningTitle" },
content: { key: "saPeopleWarningMessage" },
type: "warning",
acceptButtonText: { key: "close" },
cancelButtonText: null,
}); });
} }
} }

View File

@ -9,7 +9,6 @@ import {
ServiceAccountSecretsDetailsView, ServiceAccountSecretsDetailsView,
ServiceAccountView, ServiceAccountView,
} from "../models/view/service-account.view"; } from "../models/view/service-account.view";
import { AccessPolicyService } from "../shared/access-policies/access-policy.service";
import { import {
ServiceAccountDeleteDialogComponent, ServiceAccountDeleteDialogComponent,
@ -36,7 +35,6 @@ export class ServiceAccountsComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private dialogService: DialogService, private dialogService: DialogService,
private accessPolicyService: AccessPolicyService,
private serviceAccountService: ServiceAccountService, private serviceAccountService: ServiceAccountService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
) {} ) {}
@ -45,7 +43,6 @@ export class ServiceAccountsComponent implements OnInit {
this.serviceAccounts$ = combineLatest([ this.serviceAccounts$ = combineLatest([
this.route.params, this.route.params,
this.serviceAccountService.serviceAccount$.pipe(startWith(null)), this.serviceAccountService.serviceAccount$.pipe(startWith(null)),
this.accessPolicyService.serviceAccountAccessPolicyChanges$.pipe(startWith(null)),
]).pipe( ]).pipe(
switchMap(async ([params]) => { switchMap(async ([params]) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;

View File

@ -6,6 +6,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccessPolicySelectorService } from "./access-policy-selector.service"; import { AccessPolicySelectorService } from "./access-policy-selector.service";
import { ApItemValueType } from "./models/ap-item-value.type"; 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 { ApItemEnum } from "./models/enums/ap-item.enum";
import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; import { ApPermissionEnum } from "./models/enums/ap-permission.enum";
@ -207,6 +208,113 @@ describe("AccessPolicySelectorService", () => {
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); 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); expect(result).toBe(true);
}); });
}); });
@ -232,6 +340,16 @@ function createApItemValueType(options: Partial<ApItemValueType> = {}) {
}; };
} }
function createApItemViewType(options: Partial<ApItemViewType> = {}) {
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() { function setupUserOrg() {
const userId = "testUserId"; const userId = "testUserId";
const org = orgFactory({ userId: userId }); const org = orgFactory({ userId: userId });

View File

@ -3,6 +3,7 @@ import { Injectable } from "@angular/core";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { ApItemValueType } from "./models/ap-item-value.type"; 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 { ApItemEnum } from "./models/enums/ap-item.enum";
import { ApPermissionEnum } from "./models/enums/ap-permission.enum"; import { ApPermissionEnum } from "./models/enums/ap-permission.enum";
@ -45,4 +46,25 @@ export class AccessPolicySelectorService {
return false; 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));
}
} }

View File

@ -2,6 +2,9 @@ import {
ProjectPeopleAccessPoliciesView, ProjectPeopleAccessPoliciesView,
UserProjectAccessPolicyView, UserProjectAccessPolicyView,
GroupProjectAccessPolicyView, GroupProjectAccessPolicyView,
ServiceAccountPeopleAccessPoliciesView,
UserServiceAccountAccessPolicyView,
GroupServiceAccountAccessPolicyView,
} from "../../../../models/view/access-policy.view"; } from "../../../../models/view/access-policy.view";
import { ApItemEnum } from "./enums/ap-item.enum"; import { ApItemEnum } from "./enums/ap-item.enum";
@ -43,3 +46,33 @@ export function convertToProjectPeopleAccessPoliciesView(
}); });
return view; 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;
}

View File

@ -1,7 +1,10 @@
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SelectItemView } from "@bitwarden/components"; 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 { PotentialGranteeView } from "../../../../models/view/potential-grantee.view";
import { ApItemEnum, ApItemEnumUtil } from "./enums/ap-item.enum"; import { ApItemEnum, ApItemEnumUtil } from "./enums/ap-item.enum";
@ -29,7 +32,7 @@ export type ApItemViewType = SelectItemView & {
); );
export function convertToAccessPolicyItemViews( export function convertToAccessPolicyItemViews(
value: ProjectPeopleAccessPoliciesView, value: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView,
): ApItemViewType[] { ): ApItemViewType[] {
const accessPolicies: ApItemViewType[] = []; const accessPolicies: ApItemViewType[] = [];

View File

@ -2,7 +2,6 @@ import { Injectable } from "@angular/core";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { ListResponse } from "@bitwarden/common/models/response/list.response";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
@ -15,18 +14,16 @@ import {
GroupServiceAccountAccessPolicyView, GroupServiceAccountAccessPolicyView,
ProjectAccessPoliciesView, ProjectAccessPoliciesView,
ProjectPeopleAccessPoliciesView, ProjectPeopleAccessPoliciesView,
ServiceAccountAccessPoliciesView,
ServiceAccountProjectAccessPolicyView, ServiceAccountProjectAccessPolicyView,
UserProjectAccessPolicyView, UserProjectAccessPolicyView,
UserServiceAccountAccessPolicyView, UserServiceAccountAccessPolicyView,
ServiceAccountPeopleAccessPoliciesView,
} from "../../models/view/access-policy.view"; } from "../../models/view/access-policy.view";
import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; import { PotentialGranteeView } from "../../models/view/potential-grantee.view";
import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request"; import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request";
import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request"; import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request";
import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response"; import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response";
import { ServiceAccountAccessPoliciesResponse } from "../../shared/access-policies/models/responses/service-accounts-access-policies.response";
import { AccessSelectorRowView } from "./access-selector.component";
import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request";
import { AccessPolicyRequest } from "./models/requests/access-policy.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request";
import { GrantedPolicyRequest } from "./models/requests/granted-policy.request"; import { GrantedPolicyRequest } from "./models/requests/granted-policy.request";
@ -39,13 +36,13 @@ import {
} from "./models/responses/access-policy.response"; } from "./models/responses/access-policy.response";
import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response";
import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response"; import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response";
import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response";
@Injectable({ @Injectable({
providedIn: "root", providedIn: "root",
}) })
export class AccessPolicyService { export class AccessPolicyService {
private _projectAccessPolicyChanges$ = new Subject<ProjectAccessPoliciesView>(); private _projectAccessPolicyChanges$ = new Subject<ProjectAccessPoliciesView>();
private _serviceAccountAccessPolicyChanges$ = new Subject<ServiceAccountAccessPoliciesView>();
private _serviceAccountGrantedPolicyChanges$ = new Subject< private _serviceAccountGrantedPolicyChanges$ = new Subject<
ServiceAccountProjectAccessPolicyView[] ServiceAccountProjectAccessPolicyView[]
>(); >();
@ -55,12 +52,6 @@ export class AccessPolicyService {
*/ */
readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable(); 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. * Emits when a service account granted policy is created or deleted.
*/ */
@ -69,7 +60,6 @@ export class AccessPolicyService {
constructor( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
private organizationService: OrganizationService,
protected apiService: ApiService, protected apiService: ApiService,
protected encryptService: EncryptService, protected encryptService: EncryptService,
) {} ) {}
@ -78,10 +68,6 @@ export class AccessPolicyService {
this._projectAccessPolicyChanges$.next(null); this._projectAccessPolicyChanges$.next(null);
} }
refreshServiceAccountAccessPolicyChanges() {
this._serviceAccountAccessPolicyChanges$.next(null);
}
async getGrantedPolicies( async getGrantedPolicies(
serviceAccountId: string, serviceAccountId: string,
organizationId: string, organizationId: string,
@ -167,19 +153,35 @@ export class AccessPolicyService {
return this.createProjectPeopleAccessPoliciesView(results); return this.createProjectPeopleAccessPoliciesView(results);
} }
async getServiceAccountAccessPolicies( async getServiceAccountPeopleAccessPolicies(
serviceAccountId: string, serviceAccountId: string,
): Promise<ServiceAccountAccessPoliciesView> { ): Promise<ServiceAccountPeopleAccessPoliciesView> {
const r = await this.apiService.send( const r = await this.apiService.send(
"GET", "GET",
"/service-accounts/" + serviceAccountId + "/access-policies", "/service-accounts/" + serviceAccountId + "/access-policies/people",
null, null,
true, true,
true, true,
); );
const results = new ServiceAccountAccessPoliciesResponse(r); const results = new ServiceAccountPeopleAccessPoliciesResponse(r);
return await this.createServiceAccountAccessPoliciesView(results); 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( async createProjectAccessPolicies(
@ -201,30 +203,9 @@ export class AccessPolicyService {
return view; return view;
} }
async createServiceAccountAccessPolicies(
serviceAccountId: string,
serviceAccountAccessPoliciesView: ServiceAccountAccessPoliciesView,
): Promise<ServiceAccountAccessPoliciesView> {
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<void> { async deleteAccessPolicy(accessPolicyId: string): Promise<void> {
await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false); await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false);
this._projectAccessPolicyChanges$.next(null); this._projectAccessPolicyChanges$.next(null);
this._serviceAccountAccessPolicyChanges$.next(null);
this._serviceAccountGrantedPolicyChanges$.next(null); this._serviceAccountGrantedPolicyChanges$.next(null);
} }
@ -241,36 +222,6 @@ export class AccessPolicyService {
); );
} }
async needToShowAccessRemovalWarning(
organizationId: string,
policy: AccessSelectorRowView,
currentPolicies: AccessSelectorRowView[],
): Promise<boolean> {
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( private async createProjectAccessPoliciesView(
organizationId: string, organizationId: string,
projectAccessPoliciesResponse: ProjectAccessPoliciesResponse, projectAccessPoliciesResponse: ProjectAccessPoliciesResponse,
@ -306,6 +257,20 @@ export class AccessPolicyService {
return view; 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( private getAccessPoliciesCreateRequest(
projectAccessPoliciesView: ProjectAccessPoliciesView, projectAccessPoliciesView: ProjectAccessPoliciesView,
): AccessPoliciesCreateRequest { ): AccessPoliciesCreateRequest {
@ -337,24 +302,20 @@ export class AccessPolicyService {
} }
private getPeopleAccessPoliciesRequest( private getPeopleAccessPoliciesRequest(
projectPeopleAccessPoliciesView: ProjectPeopleAccessPoliciesView, view: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView,
): PeopleAccessPoliciesRequest { ): PeopleAccessPoliciesRequest {
const request = new PeopleAccessPoliciesRequest(); const request = new PeopleAccessPoliciesRequest();
if (projectPeopleAccessPoliciesView.userAccessPolicies?.length > 0) { if (view.userAccessPolicies?.length > 0) {
request.userAccessPolicyRequests = projectPeopleAccessPoliciesView.userAccessPolicies.map( request.userAccessPolicyRequests = view.userAccessPolicies.map((ap) => {
(ap) => { return this.getAccessPolicyRequest(ap.organizationUserId, ap);
return this.getAccessPolicyRequest(ap.organizationUserId, ap); });
},
);
} }
if (projectPeopleAccessPoliciesView.groupAccessPolicies?.length > 0) { if (view.groupAccessPolicies?.length > 0) {
request.groupAccessPolicyRequests = projectPeopleAccessPoliciesView.groupAccessPolicies.map( request.groupAccessPolicyRequests = view.groupAccessPolicies.map((ap) => {
(ap) => { return this.getAccessPolicyRequest(ap.groupId, ap);
return this.getAccessPolicyRequest(ap.groupId, ap); });
},
);
} }
return request; return request;
@ -399,50 +360,15 @@ export class AccessPolicyService {
organizationKey, organizationKey,
) )
: null, : null,
serviceAccountName: await this.encryptService.decryptToUtf8( serviceAccountName: response.serviceAccountName
new EncString(response.serviceAccountName), ? await this.encryptService.decryptToUtf8(
organizationKey, 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<ServiceAccountAccessPoliciesView> {
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( private createUserServiceAccountAccessPolicyView(
response: UserServiceAccountAccessPolicyResponse, response: UserServiceAccountAccessPolicyResponse,
): UserServiceAccountAccessPolicyView { ): UserServiceAccountAccessPolicyView {
@ -452,6 +378,7 @@ export class AccessPolicyService {
organizationUserId: response.organizationUserId, organizationUserId: response.organizationUserId,
organizationUserName: response.organizationUserName, organizationUserName: response.organizationUserName,
userId: response.userId, userId: response.userId,
currentUser: response.currentUser,
}; };
} }
@ -555,7 +482,9 @@ export class AccessPolicyService {
view.currentUserInGroup = r.currentUserInGroup; view.currentUserInGroup = r.currentUserInGroup;
if (r.type === "serviceAccount" || r.type === "project") { if (r.type === "serviceAccount" || r.type === "project") {
view.name = await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey); view.name = r.name
? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey)
: null;
} else { } else {
view.name = r.name; view.name = r.name;
} }

View File

@ -1,14 +0,0 @@
<bit-simple-dialog>
<span bitDialogTitle>{{ data.title | i18n }}</span>
<span bitDialogContent>
{{ data.message | i18n }}
</span>
<ng-container bitDialogFooter>
<button type="button" bitButton buttonType="danger" [bitAction]="removeAccess">
{{ "removeAccess" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" [bitAction]="cancel">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-simple-dialog>

View File

@ -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();
}
}
}

View File

@ -39,6 +39,7 @@ export class UserServiceAccountAccessPolicyResponse extends BaseAccessPolicyResp
organizationUserName: string; organizationUserName: string;
grantedServiceAccountId: string; grantedServiceAccountId: string;
userId: string; userId: string;
currentUser: boolean;
constructor(response: any) { constructor(response: any) {
super(response); super(response);
@ -46,6 +47,7 @@ export class UserServiceAccountAccessPolicyResponse extends BaseAccessPolicyResp
this.organizationUserName = this.getResponseProperty("OrganizationUserName"); this.organizationUserName = this.getResponseProperty("OrganizationUserName");
this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId"); this.grantedServiceAccountId = this.getResponseProperty("GrantedServiceAccountId");
this.userId = this.getResponseProperty("UserId"); this.userId = this.getResponseProperty("UserId");
this.currentUser = this.getResponseProperty("CurrentUser");
} }
} }

View File

@ -5,7 +5,7 @@ import {
UserServiceAccountAccessPolicyResponse, UserServiceAccountAccessPolicyResponse,
} from "./access-policy.response"; } from "./access-policy.response";
export class ServiceAccountAccessPoliciesResponse extends BaseResponse { export class ServiceAccountPeopleAccessPoliciesResponse extends BaseResponse {
userAccessPolicies: UserServiceAccountAccessPolicyResponse[]; userAccessPolicies: UserServiceAccountAccessPolicyResponse[];
groupAccessPolicies: GroupServiceAccountAccessPolicyResponse[]; groupAccessPolicies: GroupServiceAccountAccessPolicyResponse[];

View File

@ -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 { AccessPolicySelectorComponent } from "./access-policies/access-policy-selector/access-policy-selector.component";
import { AccessSelectorComponent } from "./access-policies/access-selector.component"; import { AccessSelectorComponent } from "./access-policies/access-selector.component";
import { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-removal-dialog.component";
import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component"; import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component";
import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component"; import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component";
import { HeaderComponent } from "./header.component"; import { HeaderComponent } from "./header.component";
@ -36,7 +35,6 @@ import { SecretsListComponent } from "./secrets-list.component";
exports: [ exports: [
SharedModule, SharedModule,
NoItemsModule, NoItemsModule,
AccessRemovalDialogComponent,
AccessSelectorComponent, AccessSelectorComponent,
AccessPolicySelectorComponent, AccessPolicySelectorComponent,
BulkStatusDialogComponent, BulkStatusDialogComponent,
@ -50,7 +48,6 @@ import { SecretsListComponent } from "./secrets-list.component";
SharedModule, SharedModule,
], ],
declarations: [ declarations: [
AccessRemovalDialogComponent,
BulkStatusDialogComponent, BulkStatusDialogComponent,
BulkConfirmationDialogComponent, BulkConfirmationDialogComponent,
HeaderComponent, HeaderComponent,