1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-22 16:29:09 +01:00

[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

@ -7437,5 +7437,8 @@
},
"collectionAccessRestricted": {
"message": "Collection access is restricted"
},
"serviceAccountAccessUpdated": {
"message": "Service account access updated"
}
}

View File

@ -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[];
}

View File

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

View File

@ -1,161 +1,92 @@
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<void>();
private serviceAccountId: string;
private organizationId: string;
private rows: AccessSelectorRowView[];
private serviceAccountId: string;
protected rows$: Observable<AccessSelectorRowView[]> =
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,
});
});
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,
});
});
return rows;
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;
}),
share(),
);
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,
private potentialGrantees$ = combineLatest([this.route.params]).pipe(
switchMap(([params]) =>
this.accessPolicyService
.getPeoplePotentialGrantees(params.organizationId)
.then((grantees) => {
return convertPotentialGranteesToApItemViewType(grantees);
}),
),
);
}
protected async handleDeleteAccessPolicy(policy: AccessSelectorRowView) {
if (
await this.accessPolicyService.needToShowAccessRemovalWarning(
this.organizationId,
policy,
this.rows,
)
) {
this.launchDeleteWarningDialog(policy);
return;
}
protected formGroup = new FormGroup({
accessPolicies: new FormControl([] as ApItemValueType[]),
});
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);
});
}
@ -164,16 +95,133 @@ export class ServiceAccountPeopleComponent {
this.destroy$.complete();
}
private launchDeleteWarningDialog(policy: AccessSelectorRowView) {
this.dialogService.open<unknown, AccessRemovalDetails>(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<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,
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;

View File

@ -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<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() {
const userId = "testUserId";
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 { 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));
}
}

View File

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

View File

@ -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[] = [];

View File

@ -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<ProjectAccessPoliciesView>();
private _serviceAccountAccessPolicyChanges$ = new Subject<ServiceAccountAccessPoliciesView>();
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<ServiceAccountAccessPoliciesView> {
): Promise<ServiceAccountPeopleAccessPoliciesView> {
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<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> {
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<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(
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) => {
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) => {
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(
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<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(
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;
}

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;
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");
}
}

View File

@ -5,7 +5,7 @@ import {
UserServiceAccountAccessPolicyResponse,
} from "./access-policy.response";
export class ServiceAccountAccessPoliciesResponse extends BaseResponse {
export class ServiceAccountPeopleAccessPoliciesResponse extends BaseResponse {
userAccessPolicies: UserServiceAccountAccessPolicyResponse[];
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 { 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,