1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-07-02 11:34:53 +02:00

[SM-654] Individual secret permissions (#9527)

* update secret service

* Add new method to access policy selector service

* Add individual secret permission management to secret dialog

* add secret.service tests
This commit is contained in:
Thomas Avery 2024-06-20 12:43:41 -05:00 committed by GitHub
parent b70d227b86
commit 1763324a89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1005 additions and 289 deletions

View File

@ -8429,5 +8429,23 @@
}, },
"providerReinstate":{ "providerReinstate":{
"message": " Contact Customer Support to reinstate your subscription." "message": " Contact Customer Support to reinstate your subscription."
},
"secretPeopleDescription": {
"message": "Grant groups or people access to this secret. Permissions set for people will override permissions set by groups."
},
"secretPeopleEmptyMessage": {
"message": "Add people or groups to share access to this secret"
},
"secretMachineAccountsDescription": {
"message": "Grant machine accounts access to this secret."
},
"secretMachineAccountsEmptyMessage": {
"message": "Add machine accounts to grant access to this secret"
},
"smAccessRemovalWarningSecretTitle": {
"message": "Remove access to this secret"
},
"smAccessRemovalSecretMessage": {
"message": "This action will remove your access to this secret."
} }
} }

View File

@ -1,71 +1,102 @@
<form [formGroup]="formGroup" [bitSubmit]="submit"> <form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large"> <bit-dialog
<ng-container bitDialogTitle>{{ title | i18n }}</ng-container> dialogSize="large"
[disablePadding]="true"
[loading]="loading"
[title]="title | i18n"
[subtitle]="subtitle"
>
<div bitDialogContent class="tw-relative"> <div bitDialogContent class="tw-relative">
<div <bit-tab-group [(selectedIndex)]="tabIndex">
*ngIf="showSpinner" <bit-tab label="{{ 'nameValuePair' | i18n }}">
class="tw-absolute tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center tw-bg-text-contrast" <div class="tw-flex tw-gap-4 tw-pt-4">
> <bit-form-field class="tw-w-1/3">
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i> <bit-label>{{ "name" | i18n }}</bit-label>
</div> <input appAutofocus formControlName="name" bitInput />
<div class="tw-flex tw-gap-4 tw-pt-4"> </bit-form-field>
<bit-form-field class="tw-w-1/3"> <bit-form-field class="tw-w-full">
<bit-label for="secret-name">{{ "name" | i18n }}</bit-label> <bit-label>{{ "value" | i18n }}</bit-label>
<input appAutofocus formControlName="name" bitInput /> <textarea bitInput rows="4" formControlName="value"></textarea>
</bit-form-field> </bit-form-field>
<bit-form-field class="tw-w-full"> </div>
<bit-label>{{ "value" | i18n }}</bit-label> <bit-form-field>
<textarea bitInput rows="4" formControlName="value"></textarea> <bit-label>{{ "notes" | i18n }}</bit-label>
</bit-form-field> <textarea bitInput rows="4" formControlName="notes"></textarea>
</div> </bit-form-field>
<bit-form-field>
<bit-label>{{ "notes" | i18n }}</bit-label>
<textarea bitInput rows="4" formControlName="notes"></textarea>
</bit-form-field>
<hr /> <hr />
<bit-form-field class="tw-mb-0 tw-mt-3"> <bit-form-field class="tw-mb-0 tw-mt-3">
<bit-label>{{ "project" | i18n }}</bit-label> <bit-label>{{ "project" | i18n }}</bit-label>
<bit-select bitInput name="project" formControlName="project"> <bit-select bitInput name="project" formControlName="project">
<bit-option value="" [label]="'selectPlaceholder' | i18n"></bit-option> <bit-option value="" [label]="'selectPlaceholder' | i18n"></bit-option>
<bit-option <bit-option
*ngFor="let p of projects" *ngFor="let p of projects"
[icon]="p.id === this.newProjectGuid ? 'bwi-plus-circle' : ''" [icon]="p.id === this.newProjectGuid ? 'bwi-plus-circle' : ''"
[value]="p.id" [value]="p.id"
[label]="p.name" [label]="p.name"
>
</bit-option>
</bit-select>
</bit-form-field>
<bit-form-field *ngIf="addNewProject == true">
<bit-label>{{ "projectName" | i18n }}</bit-label>
<input formControlName="newProjectName" bitInput />
</bit-form-field>
</bit-tab>
<bit-tab label="{{ 'people' | i18n }}">
<p>
{{ "secretPeopleDescription" | i18n }}
</p>
<sm-access-policy-selector
formControlName="peopleAccessPolicies"
[addButtonMode]="false"
[items]="peopleAccessPolicyItems"
[label]="'people' | i18n"
[hint]="'projectPeopleSelectHint' | i18n"
[columnTitle]="'name' | i18n"
[emptyMessage]="'secretPeopleEmptyMessage' | i18n"
> >
</bit-option> </sm-access-policy-selector>
</bit-select> </bit-tab>
</bit-form-field>
<bit-form-field *ngIf="addNewProject == true"> <bit-tab label="{{ 'machineAccounts' | i18n }}">
<bit-label>{{ "projectName" | i18n }}</bit-label> <p>
<input formControlName="newProjectName" bitInput /> {{ "secretMachineAccountsDescription" | i18n }}
</bit-form-field> </p>
<sm-access-policy-selector
formControlName="serviceAccountAccessPolicies"
[addButtonMode]="false"
[items]="serviceAccountAccessPolicyItems"
[label]="'machineAccounts' | i18n"
[hint]="'projectMachineAccountsSelectHint' | i18n"
[columnTitle]="'machineAccounts' | i18n"
[emptyMessage]="'secretMachineAccountsEmptyMessage' | i18n"
>
</sm-access-policy-selector>
</bit-tab>
</bit-tab-group>
</div> </div>
<ng-container bitDialogFooter> <ng-container bitDialogFooter>
<button type="submit" bitButton buttonType="primary" bitFormButton> <button [loading]="loading" type="submit" bitButton buttonType="primary" bitFormButton>
{{ "save" | i18n }} {{ "save" | i18n }}
</button> </button>
<button <button bitButton buttonType="secondary" type="button" bitDialogClose>
type="button"
bitButton
buttonType="secondary"
bitFormButton
bitDialogClose
[disabled]="false"
>
{{ "cancel" | i18n }} {{ "cancel" | i18n }}
</button> </button>
<button <button
*ngIf="deleteButtonIsVisible" *ngIf="deleteButtonIsVisible"
class="tw-ml-auto" class="tw-ml-auto"
[disabled]="loading"
type="button" type="button"
bitIconButton="bwi-trash"
buttonType="danger" buttonType="danger"
bitIconButton="bwi-trash"
bitFormButton bitFormButton
(click)="openDeleteSecretDialog()" [appA11yTitle]="'delete' | i18n"
[bitAction]="delete"
></button> ></button>
</ng-container> </ng-container>
</bit-dialog> </bit-dialog>

View File

@ -1,5 +1,5 @@
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core"; import { ChangeDetectorRef, Component, Inject, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms"; import { FormControl, FormGroup, Validators } from "@angular/forms";
import { lastValueFrom, Subject, takeUntil } from "rxjs"; import { lastValueFrom, Subject, takeUntil } from "rxjs";
@ -9,12 +9,25 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { DialogService, BitValidators } from "@bitwarden/components"; import { DialogService, BitValidators } from "@bitwarden/components";
import { SecretAccessPoliciesView } from "../../models/view/access-policies/secret-access-policies.view";
import { ProjectListView } from "../../models/view/project-list.view"; import { ProjectListView } from "../../models/view/project-list.view";
import { ProjectView } from "../../models/view/project.view"; import { ProjectView } from "../../models/view/project.view";
import { SecretListView } from "../../models/view/secret-list.view"; import { SecretListView } from "../../models/view/secret-list.view";
import { SecretProjectView } from "../../models/view/secret-project.view"; import { SecretProjectView } from "../../models/view/secret-project.view";
import { SecretView } from "../../models/view/secret.view"; import { SecretView } from "../../models/view/secret.view";
import { ProjectService } from "../../projects/project.service"; import { ProjectService } from "../../projects/project.service";
import { AccessPolicySelectorService } from "../../shared/access-policies/access-policy-selector/access-policy-selector.service";
import {
ApItemValueType,
convertToSecretAccessPoliciesView,
} from "../../shared/access-policies/access-policy-selector/models/ap-item-value.type";
import {
ApItemViewType,
convertPotentialGranteesToApItemViewType,
convertSecretAccessPoliciesToApItemViews,
} from "../../shared/access-policies/access-policy-selector/models/ap-item-view.type";
import { ApItemEnum } from "../../shared/access-policies/access-policy-selector/models/enums/ap-item.enum";
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
import { SecretService } from "../secret.service"; import { SecretService } from "../secret.service";
import { SecretDeleteDialogComponent, SecretDeleteOperation } from "./secret-delete.component"; import { SecretDeleteDialogComponent, SecretDeleteOperation } from "./secret-delete.component";
@ -24,6 +37,12 @@ export enum OperationType {
Edit, Edit,
} }
export enum SecretDialogTabType {
NameValuePair = 0,
People = 1,
ServiceAccounts = 2,
}
export interface SecretOperation { export interface SecretOperation {
organizationId: string; organizationId: string;
operation: OperationType; operation: OperationType;
@ -36,6 +55,12 @@ export interface SecretOperation {
templateUrl: "./secret-dialog.component.html", templateUrl: "./secret-dialog.component.html",
}) })
export class SecretDialogComponent implements OnInit { export class SecretDialogComponent implements OnInit {
loading = true;
projects: ProjectListView[];
addNewProject = false;
newProjectGuid = Utils.newGuid();
tabIndex: SecretDialogTabType = SecretDialogTabType.NameValuePair;
protected formGroup = new FormGroup({ protected formGroup = new FormGroup({
name: new FormControl("", { name: new FormControl("", {
validators: [Validators.required, Validators.maxLength(500), BitValidators.trimValidator], validators: [Validators.required, Validators.maxLength(500), BitValidators.trimValidator],
@ -51,77 +76,60 @@ export class SecretDialogComponent implements OnInit {
validators: [Validators.maxLength(500), BitValidators.trimValidator], validators: [Validators.maxLength(500), BitValidators.trimValidator],
updateOn: "submit", updateOn: "submit",
}), }),
peopleAccessPolicies: new FormControl([] as ApItemValueType[]),
serviceAccountAccessPolicies: new FormControl([] as ApItemValueType[]),
}); });
protected peopleAccessPolicyItems: ApItemViewType[];
protected serviceAccountAccessPolicyItems: ApItemViewType[];
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private loading = true; private currentPeopleAccessPolicies: ApItemViewType[];
projects: ProjectListView[];
addNewProject = false;
newProjectGuid = Utils.newGuid();
constructor( constructor(
public dialogRef: DialogRef, public dialogRef: DialogRef,
@Inject(DIALOG_DATA) private data: SecretOperation, @Inject(DIALOG_DATA) private data: SecretOperation,
private secretService: SecretService, private secretService: SecretService,
private changeDetectorRef: ChangeDetectorRef,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private projectService: ProjectService, private projectService: ProjectService,
private dialogService: DialogService, private dialogService: DialogService,
private organizationService: OrganizationService, private organizationService: OrganizationService,
private accessPolicyService: AccessPolicyService,
private accessPolicySelectorService: AccessPolicySelectorService,
) {} ) {}
get title() {
return this.data.operation === OperationType.Add ? "newSecret" : "editSecret";
}
get subtitle(): string | undefined {
if (this.data.operation === OperationType.Edit) {
return this.formGroup.get("name").value;
}
}
get deleteButtonIsVisible(): boolean {
return this.data.operation === OperationType.Edit;
}
async ngOnInit() { async ngOnInit() {
this.loading = true;
if (this.data.operation === OperationType.Edit && this.data.secretId) { if (this.data.operation === OperationType.Edit && this.data.secretId) {
await this.loadData(); await this.loadEditDialog();
} else if (this.data.operation !== OperationType.Add) { } else if (this.data.operation !== OperationType.Add) {
this.dialogRef.close(); this.dialogRef.close();
throw new Error(`The secret dialog was not called with the appropriate operation values.`); throw new Error(`The secret dialog was not called with the appropriate operation values.`);
} else if (this.data.operation == OperationType.Add) { } else if (this.data.operation === OperationType.Add) {
await this.loadProjects(true); await this.loadAddDialog();
if (this.data.projectId == null || this.data.projectId == "") {
this.addNewProjectOptionToProjectsDropDown();
}
}
if (this.data.projectId) {
this.formGroup.get("project").setValue(this.data.projectId);
} }
if ((await this.organizationService.get(this.data.organizationId))?.isAdmin) { if ((await this.organizationService.get(this.data.organizationId))?.isAdmin) {
this.formGroup.get("project").removeValidators(Validators.required); this.formGroup.get("project").removeValidators(Validators.required);
this.formGroup.get("project").updateValueAndValidity(); this.formGroup.get("project").updateValueAndValidity();
} }
}
async loadData() {
this.formGroup.disable();
const secret: SecretView = await this.secretService.getBySecretId(this.data.secretId);
await this.loadProjects(secret.write);
this.formGroup.setValue({
name: secret.name,
value: secret.value,
notes: secret.note,
project: secret.projects[0]?.id ?? "",
newProjectName: "",
});
this.loading = false; this.loading = false;
if (secret.write) {
this.formGroup.enable();
}
}
async loadProjects(filterByPermission: boolean) {
this.projects = await this.projectService
.getProjects(this.data.organizationId)
.then((projects) => projects.sort((a, b) => a.name.localeCompare(b.name)));
if (filterByPermission) {
this.projects = this.projects.filter((p) => p.write);
}
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -129,6 +137,152 @@ export class SecretDialogComponent implements OnInit {
this.destroy$.complete(); this.destroy$.complete();
} }
submit = async () => {
if (!this.data.organizationEnabled) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("secretsCannotCreate"));
return;
}
if (this.isFormInvalid()) {
return;
}
const secretView = this.getSecretView();
const secretAccessPoliciesView = convertToSecretAccessPoliciesView([
...this.formGroup.value.peopleAccessPolicies,
...this.formGroup.value.serviceAccountAccessPolicies,
]);
const showAccessRemovalWarning =
this.data.operation === OperationType.Edit &&
(await this.accessPolicySelectorService.showSecretAccessRemovalWarning(
this.data.organizationId,
this.currentPeopleAccessPolicies,
this.formGroup.value.peopleAccessPolicies,
));
if (showAccessRemovalWarning) {
const confirmed = await this.showWarning();
if (!confirmed) {
return;
}
}
if (this.addNewProject) {
const newProject = await this.createProject(this.getNewProjectView());
secretView.projects = [newProject];
}
if (this.data.operation === OperationType.Add) {
await this.createSecret(secretView, secretAccessPoliciesView);
} else {
secretView.id = this.data.secretId;
await this.updateSecret(secretView, secretAccessPoliciesView);
}
this.dialogRef.close();
};
delete = async () => {
const secretListView: SecretListView[] = this.getSecretListView();
const dialogRef = this.dialogService.open<unknown, SecretDeleteOperation>(
SecretDeleteDialogComponent,
{
data: {
secrets: secretListView,
},
},
);
await lastValueFrom(dialogRef.closed).then(
(closeData) => closeData !== undefined && this.dialogRef.close(),
);
};
private async loadEditDialog() {
const secret = await this.secretService.getBySecretId(this.data.secretId);
await this.loadProjects(secret.projects);
const currentAccessPolicies = await this.getCurrentAccessPolicies(
this.data.organizationId,
this.data.secretId,
);
this.currentPeopleAccessPolicies = currentAccessPolicies.filter(
(p) => p.type === ApItemEnum.User || p.type === ApItemEnum.Group,
);
const currentServiceAccountPolicies = currentAccessPolicies.filter(
(p) => p.type === ApItemEnum.ServiceAccount,
);
this.peopleAccessPolicyItems = await this.getPeoplePotentialGrantees(this.data.organizationId);
this.serviceAccountAccessPolicyItems = await this.getServiceAccountItems(
this.data.organizationId,
currentServiceAccountPolicies,
);
// 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({
name: secret.name,
value: secret.value,
notes: secret.note,
project: secret.projects[0]?.id ?? "",
newProjectName: "",
peopleAccessPolicies: this.currentPeopleAccessPolicies.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,
})),
serviceAccountAccessPolicies: currentServiceAccountPolicies.map((m) => ({
type: m.type,
id: m.id,
permission: m.permission,
})),
});
}
private async loadAddDialog() {
await this.loadProjects();
this.peopleAccessPolicyItems = await this.getPeoplePotentialGrantees(this.data.organizationId);
this.serviceAccountAccessPolicyItems = await this.getServiceAccountItems(
this.data.organizationId,
);
if (
this.data.projectId === null ||
this.data.projectId === "" ||
this.data.projectId === undefined
) {
this.addNewProjectOptionToProjectsDropDown();
}
if (this.data.projectId) {
this.formGroup.get("project").setValue(this.data.projectId);
}
}
private async loadProjects(currentProjects?: SecretProjectView[]) {
this.projects = await this.projectService
.getProjects(this.data.organizationId)
.then((projects) => projects.filter((p) => p.write));
if (currentProjects?.length > 0) {
const currentProject = currentProjects?.[0];
if (this.projects.find((p) => p.id === currentProject.id) === undefined) {
const listView = new ProjectListView();
listView.id = currentProject.id;
listView.name = currentProject.name;
this.projects.push(listView);
}
}
this.projects = this.projects.sort((a, b) => a.name.localeCompare(b.name));
}
private addNewProjectOptionToProjectsDropDown() { private addNewProjectOptionToProjectsDropDown() {
this.formGroup this.formGroup
.get("project") .get("project")
@ -155,70 +309,15 @@ export class SecretDialogComponent implements OnInit {
this.formGroup.get("newProjectName").updateValueAndValidity(); this.formGroup.get("newProjectName").updateValueAndValidity();
} }
get title() {
return this.data.operation === OperationType.Add ? "newSecret" : "editSecret";
}
get showSpinner() {
return this.data.operation === OperationType.Edit && this.loading;
}
submit = async () => {
if (!this.data.organizationEnabled) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("secretsCannotCreate"));
return;
}
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
return;
}
const secretView = this.getSecretView();
if (this.addNewProject) {
const newProject = await this.createProject(this.getNewProjectView());
secretView.projects = [newProject];
}
if (this.data.operation === OperationType.Add) {
await this.createSecret(secretView);
} else {
secretView.id = this.data.secretId;
await this.updateSecret(secretView);
}
this.dialogRef.close();
};
get deleteButtonIsVisible(): boolean {
return this.data.operation === OperationType.Edit;
}
private async createProject(projectView: ProjectView) { private async createProject(projectView: ProjectView) {
return await this.projectService.create(this.data.organizationId, projectView); return await this.projectService.create(this.data.organizationId, projectView);
} }
protected async openDeleteSecretDialog() { private async createSecret(
const secretListView: SecretListView[] = this.getSecretListView(); secretView: SecretView,
secretAccessPoliciesView: SecretAccessPoliciesView,
const dialogRef = this.dialogService.open<unknown, SecretDeleteOperation>( ) {
SecretDeleteDialogComponent, await this.secretService.create(this.data.organizationId, secretView, secretAccessPoliciesView);
{
data: {
secrets: secretListView,
},
},
);
// If the secret is deleted, chain close this dialog after the delete dialog
await lastValueFrom(dialogRef.closed).then(
(closeData) => closeData !== undefined && this.dialogRef.close(),
);
}
private async createSecret(secretView: SecretView) {
await this.secretService.create(this.data.organizationId, secretView);
this.platformUtilsService.showToast("success", null, this.i18nService.t("secretCreated")); this.platformUtilsService.showToast("success", null, this.i18nService.t("secretCreated"));
} }
@ -229,8 +328,11 @@ export class SecretDialogComponent implements OnInit {
return projectView; return projectView;
} }
private async updateSecret(secretView: SecretView) { private async updateSecret(
await this.secretService.update(this.data.organizationId, secretView); secretView: SecretView,
secretAccessPoliciesView: SecretAccessPoliciesView,
) {
await this.secretService.update(this.data.organizationId, secretView, secretAccessPoliciesView);
this.platformUtilsService.showToast("success", null, this.i18nService.t("secretEdited")); this.platformUtilsService.showToast("success", null, this.i18nService.t("secretEdited"));
} }
@ -265,4 +367,63 @@ export class SecretDialogComponent implements OnInit {
secretListViews.push(secretListView); secretListViews.push(secretListView);
return secretListViews; return secretListViews;
} }
private async getCurrentAccessPolicies(
organizationId: string,
secretId: string,
): Promise<ApItemViewType[]> {
return convertSecretAccessPoliciesToApItemViews(
await this.accessPolicyService.getSecretAccessPolicies(organizationId, secretId),
);
}
private async getPeoplePotentialGrantees(organizationId: string): Promise<ApItemViewType[]> {
return convertPotentialGranteesToApItemViewType(
await this.accessPolicyService.getPeoplePotentialGrantees(organizationId),
);
}
private async getServiceAccountItems(
organizationId: string,
currentAccessPolicies?: ApItemViewType[],
): Promise<ApItemViewType[]> {
const potentialGrantees = convertPotentialGranteesToApItemViewType(
await this.accessPolicyService.getServiceAccountsPotentialGrantees(organizationId),
);
const items = [...potentialGrantees];
if (currentAccessPolicies) {
for (const policy of currentAccessPolicies) {
const exists = potentialGrantees.some((grantee) => grantee.id === policy.id);
if (!exists) {
items.push(policy);
}
}
}
return items;
}
private async showWarning(): Promise<boolean> {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "smAccessRemovalWarningSecretTitle" },
content: { key: "smAccessRemovalSecretMessage" },
acceptButtonText: { key: "removeAccess" },
cancelButtonText: { key: "cancel" },
type: "warning",
});
return confirmed;
}
private isFormInvalid(): boolean {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid && this.tabIndex !== SecretDialogTabType.NameValuePair) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("nameValuePair")),
);
}
return this.formGroup.invalid;
}
} }

View File

@ -1,6 +1,9 @@
import { SecretAccessPoliciesRequest } from "../../shared/access-policies/models/requests/secret-access-policies.request";
export class SecretRequest { export class SecretRequest {
key: string; key: string;
value: string; value: string;
note: string; note: string;
projectIds?: string[]; projectIds?: string[];
accessPoliciesRequests: SecretAccessPoliciesRequest;
} }

View File

@ -0,0 +1,125 @@
import { mock } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SecretAccessPoliciesView } from "../models/view/access-policies/secret-access-policies.view";
import { SecretView } from "../models/view/secret.view";
import { AccessPolicyService } from "../shared/access-policies/access-policy.service";
import { SecretService } from "./secret.service";
describe("SecretService", () => {
let sut: SecretService;
const cryptoService = mock<CryptoService>();
const apiService = mock<ApiService>();
const encryptService = mock<EncryptService>();
const accessPolicyService = mock<AccessPolicyService>();
beforeEach(() => {
jest.resetAllMocks();
sut = new SecretService(cryptoService, apiService, encryptService, accessPolicyService);
encryptService.encrypt.mockResolvedValue({
encryptedString: "mockEncryptedString",
} as EncString);
encryptService.decryptToUtf8.mockResolvedValue(mockUnencryptedData);
});
it("instantiates", () => {
expect(sut).not.toBeFalsy();
});
describe("create", () => {
it("emits the secret created", async () => {
apiService.send.mockResolvedValue(mockedSecretResponse);
sut.secret$.subscribe((secret) => {
expect(secret).toBeDefined();
expect(secret).toEqual(expectedSecretView);
});
await sut.create("organizationId", secretView, secretAccessPoliciesView);
});
});
describe("update", () => {
it("emits the secret updated", async () => {
apiService.send.mockResolvedValue(mockedSecretResponse);
sut.secret$.subscribe((secret) => {
expect(secret).toBeDefined();
expect(secret).toEqual(expectedSecretView);
});
await sut.update("organizationId", secretView, secretAccessPoliciesView);
});
});
});
const mockedSecretResponse: any = {
id: "001f835c-aa41-4f25-bfbf-b18d0103a1db",
organizationId: "da0eea55-8604-4307-8a24-b187015e3071",
key: "mockEncryptedString",
value: "mockEncryptedString",
note: "mockEncryptedString",
creationDate: "2024-07-12T15:45:17.49823Z",
revisionDate: "2024-07-12T15:45:17.49823Z",
projects: [
{
id: "502d93ae-a084-490a-8a64-b187015eb69c",
name: "mockEncryptedString",
},
],
read: true,
write: true,
object: "secret",
};
const secretView: SecretView = {
id: "001f835c-aa41-4f25-bfbf-b18d0103a1db",
organizationId: "da0eea55-8604-4307-8a24-b187015e3071",
name: "key",
value: "value",
note: "note",
creationDate: "2024-06-12T15:45:17.49823Z",
revisionDate: "2024-06-12T15:45:17.49823Z",
projects: [
{
id: "502d93ae-a084-490a-8a64-b187015eb69c",
name: "project-name",
},
],
read: true,
write: true,
};
const secretAccessPoliciesView: SecretAccessPoliciesView = {
userAccessPolicies: [],
groupAccessPolicies: [],
serviceAccountAccessPolicies: [],
};
const mockUnencryptedData = "mockUnEncryptedString";
const expectedSecretView: SecretView = {
id: "001f835c-aa41-4f25-bfbf-b18d0103a1db",
organizationId: "da0eea55-8604-4307-8a24-b187015e3071",
name: mockUnencryptedData,
value: mockUnencryptedData,
note: mockUnencryptedData,
creationDate: "2024-07-12T15:45:17.49823Z",
revisionDate: "2024-07-12T15:45:17.49823Z",
projects: [
{
id: "502d93ae-a084-490a-8a64-b187015eb69c",
name: mockUnencryptedData,
},
],
read: true,
write: true,
};

View File

@ -7,9 +7,11 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { SecretAccessPoliciesView } from "../models/view/access-policies/secret-access-policies.view";
import { SecretListView } from "../models/view/secret-list.view"; import { SecretListView } from "../models/view/secret-list.view";
import { SecretProjectView } from "../models/view/secret-project.view"; import { SecretProjectView } from "../models/view/secret-project.view";
import { SecretView } from "../models/view/secret.view"; import { SecretView } from "../models/view/secret.view";
import { AccessPolicyService } from "../shared/access-policies/access-policy.service";
import { BulkOperationStatus } from "../shared/dialogs/bulk-status-dialog.component"; import { BulkOperationStatus } from "../shared/dialogs/bulk-status-dialog.component";
import { SecretRequest } from "./requests/secret.request"; import { SecretRequest } from "./requests/secret.request";
@ -30,6 +32,7 @@ export class SecretService {
private cryptoService: CryptoService, private cryptoService: CryptoService,
private apiService: ApiService, private apiService: ApiService,
private encryptService: EncryptService, private encryptService: EncryptService,
private accessPolicyService: AccessPolicyService,
) {} ) {}
async getBySecretId(secretId: string): Promise<SecretView> { async getBySecretId(secretId: string): Promise<SecretView> {
@ -65,8 +68,16 @@ export class SecretService {
return await this.createSecretsListView(organizationId, results); return await this.createSecretsListView(organizationId, results);
} }
async create(organizationId: string, secretView: SecretView) { async create(
const request = await this.getSecretRequest(organizationId, secretView); organizationId: string,
secretView: SecretView,
secretAccessPoliciesView: SecretAccessPoliciesView,
) {
const request = await this.getSecretRequest(
organizationId,
secretView,
secretAccessPoliciesView,
);
const r = await this.apiService.send( const r = await this.apiService.send(
"POST", "POST",
"/organizations/" + organizationId + "/secrets", "/organizations/" + organizationId + "/secrets",
@ -77,8 +88,16 @@ export class SecretService {
this._secret.next(await this.createSecretView(new SecretResponse(r))); this._secret.next(await this.createSecretView(new SecretResponse(r)));
} }
async update(organizationId: string, secretView: SecretView) { async update(
const request = await this.getSecretRequest(organizationId, secretView); organizationId: string,
secretView: SecretView,
secretAccessPoliciesView: SecretAccessPoliciesView,
) {
const request = await this.getSecretRequest(
organizationId,
secretView,
secretAccessPoliciesView,
);
const r = await this.apiService.send("PUT", "/secrets/" + secretView.id, request, true, true); const r = await this.apiService.send("PUT", "/secrets/" + secretView.id, request, true, true);
this._secret.next(await this.createSecretView(new SecretResponse(r))); this._secret.next(await this.createSecretView(new SecretResponse(r)));
} }
@ -140,6 +159,7 @@ export class SecretService {
private async getSecretRequest( private async getSecretRequest(
organizationId: string, organizationId: string,
secretView: SecretView, secretView: SecretView,
secretAccessPoliciesView: SecretAccessPoliciesView,
): Promise<SecretRequest> { ): Promise<SecretRequest> {
const orgKey = await this.getOrganizationKey(organizationId); const orgKey = await this.getOrganizationKey(organizationId);
const request = new SecretRequest(); const request = new SecretRequest();
@ -155,6 +175,9 @@ export class SecretService {
secretView.projects?.forEach((e) => request.projectIds.push(e.id)); secretView.projects?.forEach((e) => request.projectIds.push(e.id));
request.accessPoliciesRequests =
this.accessPolicyService.getSecretAccessPoliciesRequest(secretAccessPoliciesView);
return request; return request;
} }

View File

@ -64,10 +64,12 @@ describe("AccessPolicySelectorService", () => {
const selectedPolicyValues: ApItemValueType[] = []; const selectedPolicyValues: ApItemValueType[] = [];
selectedPolicyValues.push( selectedPolicyValues.push(
createApItemValueType({ createApItemValueType(
permission: ApPermissionEnum.CanRead, {
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
true,
),
); );
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
@ -80,15 +82,17 @@ describe("AccessPolicySelectorService", () => {
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockResolvedValue(org);
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
permission: ApPermissionEnum.CanReadWrite, {
currentUser: true, permission: ApPermissionEnum.CanReadWrite,
}), },
true,
),
]; ];
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
expect(result).toBe(true); expect(result).toBe(false);
}); });
it("returns true when current user isn't owner/admin and a group Read policy is submitted that the user is a member of", async () => { it("returns true when current user isn't owner/admin and a group Read policy is submitted that the user is a member of", async () => {
@ -96,12 +100,15 @@ describe("AccessPolicySelectorService", () => {
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockResolvedValue(org);
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
id: "groupId", {
type: ApItemEnum.Group, id: "groupId",
permission: ApPermissionEnum.CanRead, type: ApItemEnum.Group,
currentUserInGroup: true, permission: ApPermissionEnum.CanRead,
}), },
false,
true,
),
]; ];
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
@ -114,12 +121,15 @@ describe("AccessPolicySelectorService", () => {
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockResolvedValue(org);
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
id: "groupId", {
type: ApItemEnum.Group, id: "groupId",
permission: ApPermissionEnum.CanReadWrite, type: ApItemEnum.Group,
currentUserInGroup: true, permission: ApPermissionEnum.CanReadWrite,
}), },
false,
true,
),
]; ];
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
@ -132,12 +142,15 @@ describe("AccessPolicySelectorService", () => {
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockResolvedValue(org);
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
id: "groupId", {
type: ApItemEnum.Group, id: "groupId",
permission: ApPermissionEnum.CanReadWrite, type: ApItemEnum.Group,
currentUserInGroup: false, permission: ApPermissionEnum.CanReadWrite,
}), },
false,
false,
),
]; ];
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
@ -150,16 +163,21 @@ describe("AccessPolicySelectorService", () => {
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockResolvedValue(org);
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
permission: ApPermissionEnum.CanRead, {
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
createApItemValueType({ true,
id: "groupId", ),
type: ApItemEnum.Group, createApItemValueType(
permission: ApPermissionEnum.CanReadWrite, {
currentUserInGroup: true, id: "groupId",
}), type: ApItemEnum.Group,
permission: ApPermissionEnum.CanReadWrite,
},
false,
true,
),
]; ];
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
@ -172,16 +190,21 @@ describe("AccessPolicySelectorService", () => {
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockResolvedValue(org);
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
permission: ApPermissionEnum.CanRead, {
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
createApItemValueType({ true,
id: "groupId", ),
type: ApItemEnum.Group, createApItemValueType(
permission: ApPermissionEnum.CanReadWrite, {
currentUserInGroup: false, id: "groupId",
}), type: ApItemEnum.Group,
permission: ApPermissionEnum.CanReadWrite,
},
false,
false,
),
]; ];
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
@ -194,16 +217,21 @@ describe("AccessPolicySelectorService", () => {
organizationService.get.calledWith(org.id).mockResolvedValue(org); organizationService.get.calledWith(org.id).mockResolvedValue(org);
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
permission: ApPermissionEnum.CanRead, {
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
createApItemValueType({ true,
id: "groupId", ),
type: ApItemEnum.Group, createApItemValueType(
permission: ApPermissionEnum.CanRead, {
currentUserInGroup: true, id: "groupId",
}), type: ApItemEnum.Group,
permission: ApPermissionEnum.CanRead,
},
false,
true,
),
]; ];
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues); const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
@ -211,6 +239,246 @@ describe("AccessPolicySelectorService", () => {
expect(result).toBe(true); expect(result).toBe(true);
}); });
}); });
describe("showSecretAccessRemovalWarning", () => {
it("returns false when there are no current access policies", async () => {
const org = orgFactory();
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const currentAccessPolicies: ApItemViewType[] = [];
const selectedPolicyValues: ApItemValueType[] = [];
const result = await sut.showSecretAccessRemovalWarning(
org.id,
currentAccessPolicies,
selectedPolicyValues,
);
expect(result).toBe(false);
});
it("returns false when current user is admin", async () => {
const org = orgFactory();
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType(
{
id: "example",
permission: ApPermissionEnum.CanRead,
},
true,
),
];
const selectedPolicyValues: ApItemValueType[] = [];
const result = await sut.showSecretAccessRemovalWarning(
org.id,
currentAccessPolicies,
selectedPolicyValues,
);
expect(result).toBe(false);
});
it("returns false when current user is owner", async () => {
const org = orgFactory();
org.type = OrganizationUserType.Owner;
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType(
{
id: "example",
permission: ApPermissionEnum.CanRead,
},
true,
),
];
const selectedPolicyValues: ApItemValueType[] = [];
const result = await sut.showSecretAccessRemovalWarning(
org.id,
currentAccessPolicies,
selectedPolicyValues,
);
expect(result).toBe(false);
});
it("returns false when current non-admin user doesn't have Read, Write access with current access policies -- user policy", async () => {
const org = setupUserOrg();
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType(
{
id: "example",
permission: ApPermissionEnum.CanRead,
},
true,
),
];
const selectedPolicyValues: ApItemValueType[] = [];
const result = await sut.showSecretAccessRemovalWarning(
org.id,
currentAccessPolicies,
selectedPolicyValues,
);
expect(result).toBe(false);
});
it("returns false when current non-admin user doesn't have Read, Write access with current access policies -- group policy", async () => {
const org = setupUserOrg();
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType(
{
id: "example",
type: ApItemEnum.Group,
permission: ApPermissionEnum.CanRead,
},
false,
true,
),
];
const selectedPolicyValues: ApItemValueType[] = [];
const result = await sut.showSecretAccessRemovalWarning(
org.id,
currentAccessPolicies,
selectedPolicyValues,
);
expect(result).toBe(false);
});
it("returns true when current non-admin user has Read, Write access with current access policies and doesn't with selected -- user policy", async () => {
const org = setupUserOrg();
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType(
{
id: "example",
permission: ApPermissionEnum.CanReadWrite,
},
true,
),
];
const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({
type: ApItemEnum.User,
permission: ApPermissionEnum.CanRead,
currentUser: true,
}),
];
const result = await sut.showSecretAccessRemovalWarning(
org.id,
currentAccessPolicies,
selectedPolicyValues,
);
expect(result).toBe(true);
});
it("returns true when current non-admin user has Read, Write access with current access policies and doesn't with selected -- group policy", async () => {
const org = setupUserOrg();
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType(
{
id: "example",
type: ApItemEnum.Group,
permission: ApPermissionEnum.CanReadWrite,
},
false,
true,
),
];
const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType(
{
type: ApItemEnum.Group,
permission: ApPermissionEnum.CanRead,
},
false,
true,
),
];
const result = await sut.showSecretAccessRemovalWarning(
org.id,
currentAccessPolicies,
selectedPolicyValues,
);
expect(result).toBe(true);
});
it("returns false when current non-admin user has Read, Write access with current access policies and does with selected -- user policy", async () => {
const org = setupUserOrg();
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType(
{
id: "example",
permission: ApPermissionEnum.CanReadWrite,
},
true,
),
];
const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType(
{
type: ApItemEnum.User,
permission: ApPermissionEnum.CanReadWrite,
},
true,
),
];
const result = await sut.showSecretAccessRemovalWarning(
org.id,
currentAccessPolicies,
selectedPolicyValues,
);
expect(result).toBe(false);
});
it("returns false when current non-admin user has Read, Write access with current access policies and does with selected -- group policy", async () => {
const org = setupUserOrg();
organizationService.get.calledWith(org.id).mockResolvedValue(org);
const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType(
{
id: "example",
type: ApItemEnum.Group,
permission: ApPermissionEnum.CanReadWrite,
},
false,
true,
),
];
const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType(
{
type: ApItemEnum.Group,
permission: ApPermissionEnum.CanReadWrite,
},
false,
true,
),
];
const result = await sut.showSecretAccessRemovalWarning(
org.id,
currentAccessPolicies,
selectedPolicyValues,
);
expect(result).toBe(false);
});
});
describe("isAccessRemoval", () => { describe("isAccessRemoval", () => {
it("returns false when there are no previous policies and no selected policies", async () => { it("returns false when there are no previous policies and no selected policies", async () => {
const currentAccessPolicies: ApItemViewType[] = []; const currentAccessPolicies: ApItemViewType[] = [];
@ -223,11 +491,13 @@ describe("AccessPolicySelectorService", () => {
it("returns false when there are no previous policies", async () => { it("returns false when there are no previous policies", async () => {
const currentAccessPolicies: ApItemViewType[] = []; const currentAccessPolicies: ApItemViewType[] = [];
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
id: "example", {
permission: ApPermissionEnum.CanRead, id: "example",
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
true,
),
]; ];
const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues);
@ -236,18 +506,22 @@ describe("AccessPolicySelectorService", () => {
}); });
it("returns false when previous policies and selected policies are the same", async () => { it("returns false when previous policies and selected policies are the same", async () => {
const currentAccessPolicies: ApItemViewType[] = [ const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType({ createApItemViewType(
id: "example", {
permission: ApPermissionEnum.CanRead, id: "example",
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
true,
),
]; ];
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
id: "example", {
permission: ApPermissionEnum.CanRead, id: "example",
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
true,
),
]; ];
const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues);
@ -256,23 +530,29 @@ describe("AccessPolicySelectorService", () => {
}); });
it("returns false when previous policies are still selected", async () => { it("returns false when previous policies are still selected", async () => {
const currentAccessPolicies: ApItemViewType[] = [ const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType({ createApItemViewType(
id: "example", {
permission: ApPermissionEnum.CanRead, id: "example",
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
true,
),
]; ];
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
id: "example", {
permission: ApPermissionEnum.CanRead, id: "example",
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
createApItemValueType({ true,
id: "example-2", ),
permission: ApPermissionEnum.CanRead, createApItemValueType(
currentUser: true, {
}), id: "example-2",
permission: ApPermissionEnum.CanRead,
},
true,
),
]; ];
const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues);
@ -281,23 +561,29 @@ describe("AccessPolicySelectorService", () => {
}); });
it("returns true when previous policies are not selected", async () => { it("returns true when previous policies are not selected", async () => {
const currentAccessPolicies: ApItemViewType[] = [ const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType({ createApItemViewType(
id: "example", {
permission: ApPermissionEnum.CanRead, id: "example",
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
true,
),
]; ];
const selectedPolicyValues: ApItemValueType[] = [ const selectedPolicyValues: ApItemValueType[] = [
createApItemValueType({ createApItemValueType(
id: "test", {
permission: ApPermissionEnum.CanRead, id: "test",
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
createApItemValueType({ true,
id: "example-2", ),
permission: ApPermissionEnum.CanRead, createApItemValueType(
currentUser: true, {
}), id: "example-2",
permission: ApPermissionEnum.CanRead,
},
true,
),
]; ];
const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues); const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues);
@ -306,10 +592,12 @@ describe("AccessPolicySelectorService", () => {
}); });
it("returns true when there are previous policies and nothing was selected", async () => { it("returns true when there are previous policies and nothing was selected", async () => {
const currentAccessPolicies: ApItemViewType[] = [ const currentAccessPolicies: ApItemViewType[] = [
createApItemViewType({ createApItemViewType(
permission: ApPermissionEnum.CanRead, {
currentUser: true, permission: ApPermissionEnum.CanRead,
}), },
true,
),
]; ];
const selectedPolicyValues: ApItemValueType[] = []; const selectedPolicyValues: ApItemValueType[] = [];
@ -331,17 +619,31 @@ const orgFactory = (props: Partial<Organization> = {}) =>
props, props,
); );
function createApItemValueType(options: Partial<ApItemValueType> = {}) { function createApItemValueType(
return { options: Partial<ApItemValueType> = {},
currentUser = false,
currentUserInGroup = false,
) {
const item: ApItemValueType = {
id: options?.id ?? "test", id: options?.id ?? "test",
type: options?.type ?? ApItemEnum.User, type: options?.type ?? ApItemEnum.User,
permission: options?.permission ?? ApPermissionEnum.CanRead, permission: options?.permission ?? ApPermissionEnum.CanRead,
currentUserInGroup: options?.currentUserInGroup ?? false,
}; };
if (item.type === ApItemEnum.User) {
item.currentUser = currentUser;
}
if (item.type === ApItemEnum.Group) {
item.currentUserInGroup = currentUserInGroup;
}
return item;
} }
function createApItemViewType(options: Partial<ApItemViewType> = {}) { function createApItemViewType(
return { options: Partial<ApItemViewType> = {},
currentUser = false,
currentUserInGroup = false,
) {
const item: ApItemViewType = {
id: options?.id ?? "test", id: options?.id ?? "test",
listName: options?.listName ?? "test", listName: options?.listName ?? "test",
labelName: options?.labelName ?? "test", labelName: options?.labelName ?? "test",
@ -349,6 +651,13 @@ function createApItemViewType(options: Partial<ApItemViewType> = {}) {
permission: options?.permission ?? ApPermissionEnum.CanRead, permission: options?.permission ?? ApPermissionEnum.CanRead,
readOnly: options?.readOnly ?? false, readOnly: options?.readOnly ?? false,
}; };
if (item.type === ApItemEnum.User) {
item.currentUser = currentUser;
}
if (item.type === ApItemEnum.Group) {
item.currentUserInGroup = currentUserInGroup;
}
return item;
} }
function setupUserOrg() { function setupUserOrg() {

View File

@ -22,26 +22,29 @@ export class AccessPolicySelectorService {
return false; return false;
} }
const selectedUserReadWritePolicy = selectedPoliciesValues.find( if (!this.userHasReadWriteAccess(selectedPoliciesValues)) {
(s) => return true;
s.type === ApItemEnum.User && }
s.currentUser &&
s.permission === ApPermissionEnum.CanReadWrite,
);
const selectedGroupReadWritePolicies = selectedPoliciesValues.filter( return false;
(s) => }
s.type === ApItemEnum.Group &&
s.permission == ApPermissionEnum.CanReadWrite &&
s.currentUserInGroup,
);
if (selectedGroupReadWritePolicies == null || selectedGroupReadWritePolicies.length == 0) { async showSecretAccessRemovalWarning(
if (selectedUserReadWritePolicy == null) { organizationId: string,
return true; current: ApItemViewType[],
} else { selectedPoliciesValues: ApItemValueType[],
return false; ): Promise<boolean> {
} if (current.length === 0) {
return false;
}
const organization = await this.organizationService.get(organizationId);
if (organization.isOwner || organization.isAdmin || !this.userHasReadWriteAccess(current)) {
return false;
}
if (!this.userHasReadWriteAccess(selectedPoliciesValues)) {
return true;
} }
return false; return false;
@ -67,4 +70,25 @@ export class AccessPolicySelectorService {
const selectedIds = selected.map((x) => x.id); const selectedIds = selected.map((x) => x.id);
return !currentIds.every((id) => selectedIds.includes(id)); return !currentIds.every((id) => selectedIds.includes(id));
} }
private userHasReadWriteAccess(policies: ApItemValueType[] | ApItemViewType[]): boolean {
const userReadWritePolicy = (policies as Array<ApItemValueType | ApItemViewType>).find(
(s) =>
s.type === ApItemEnum.User &&
s.currentUser &&
s.permission === ApPermissionEnum.CanReadWrite,
);
const groupReadWritePolicies = (policies as Array<ApItemValueType | ApItemViewType>).filter(
(s) =>
s.type === ApItemEnum.Group &&
s.permission === ApPermissionEnum.CanReadWrite &&
s.currentUserInGroup,
);
if (groupReadWritePolicies.length > 0 || userReadWritePolicy !== undefined) {
return true;
}
return false;
}
} }

View File

@ -27,6 +27,7 @@ import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/
import { AccessPolicyRequest } from "./models/requests/access-policy.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request";
import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request"; import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request";
import { SecretAccessPoliciesRequest } from "./models/requests/secret-access-policies.request";
import { import {
GroupAccessPolicyResponse, GroupAccessPolicyResponse,
UserAccessPolicyResponse, UserAccessPolicyResponse,
@ -233,6 +234,20 @@ export class AccessPolicyService {
return await this.createPotentialGranteeViews(organizationId, results.data); return await this.createPotentialGranteeViews(organizationId, results.data);
} }
getSecretAccessPoliciesRequest(view: SecretAccessPoliciesView): SecretAccessPoliciesRequest {
return {
userAccessPolicyRequests: view.userAccessPolicies.map((ap) => {
return this.getAccessPolicyRequest(ap.organizationUserId, ap);
}),
groupAccessPolicyRequests: view.groupAccessPolicies.map((ap) => {
return this.getAccessPolicyRequest(ap.groupId, ap);
}),
serviceAccountAccessPolicyRequests: view.serviceAccountAccessPolicies.map((ap) => {
return this.getAccessPolicyRequest(ap.serviceAccountId, ap);
}),
};
}
private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> { private async getOrganizationKey(organizationId: string): Promise<SymmetricCryptoKey> {
return await this.cryptoService.getOrgKey(organizationId); return await this.cryptoService.getOrgKey(organizationId);
} }

View File

@ -0,0 +1,7 @@
import { AccessPolicyRequest } from "./access-policy.request";
export class SecretAccessPoliciesRequest {
userAccessPolicyRequests: AccessPolicyRequest[];
groupAccessPolicyRequests: AccessPolicyRequest[];
serviceAccountAccessPolicyRequests: AccessPolicyRequest[];
}