mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-18 20:41:31 +01: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:
parent
b70d227b86
commit
1763324a89
@ -8429,5 +8429,23 @@
|
||||
},
|
||||
"providerReinstate":{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
@ -1,71 +1,102 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="large">
|
||||
<ng-container bitDialogTitle>{{ title | i18n }}</ng-container>
|
||||
<bit-dialog
|
||||
dialogSize="large"
|
||||
[disablePadding]="true"
|
||||
[loading]="loading"
|
||||
[title]="title | i18n"
|
||||
[subtitle]="subtitle"
|
||||
>
|
||||
<div bitDialogContent class="tw-relative">
|
||||
<div
|
||||
*ngIf="showSpinner"
|
||||
class="tw-absolute tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center tw-bg-text-contrast"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
||||
</div>
|
||||
<div class="tw-flex tw-gap-4 tw-pt-4">
|
||||
<bit-form-field class="tw-w-1/3">
|
||||
<bit-label for="secret-name">{{ "name" | i18n }}</bit-label>
|
||||
<input appAutofocus formControlName="name" bitInput />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-w-full">
|
||||
<bit-label>{{ "value" | i18n }}</bit-label>
|
||||
<textarea bitInput rows="4" formControlName="value"></textarea>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "notes" | i18n }}</bit-label>
|
||||
<textarea bitInput rows="4" formControlName="notes"></textarea>
|
||||
</bit-form-field>
|
||||
<bit-tab-group [(selectedIndex)]="tabIndex">
|
||||
<bit-tab label="{{ 'nameValuePair' | i18n }}">
|
||||
<div class="tw-flex tw-gap-4 tw-pt-4">
|
||||
<bit-form-field class="tw-w-1/3">
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input appAutofocus formControlName="name" bitInput />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-w-full">
|
||||
<bit-label>{{ "value" | i18n }}</bit-label>
|
||||
<textarea bitInput rows="4" formControlName="value"></textarea>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
<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-label>{{ "project" | i18n }}</bit-label>
|
||||
<bit-select bitInput name="project" formControlName="project">
|
||||
<bit-option value="" [label]="'selectPlaceholder' | i18n"></bit-option>
|
||||
<bit-option
|
||||
*ngFor="let p of projects"
|
||||
[icon]="p.id === this.newProjectGuid ? 'bwi-plus-circle' : ''"
|
||||
[value]="p.id"
|
||||
[label]="p.name"
|
||||
<bit-form-field class="tw-mb-0 tw-mt-3">
|
||||
<bit-label>{{ "project" | i18n }}</bit-label>
|
||||
<bit-select bitInput name="project" formControlName="project">
|
||||
<bit-option value="" [label]="'selectPlaceholder' | i18n"></bit-option>
|
||||
<bit-option
|
||||
*ngFor="let p of projects"
|
||||
[icon]="p.id === this.newProjectGuid ? 'bwi-plus-circle' : ''"
|
||||
[value]="p.id"
|
||||
[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>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</sm-access-policy-selector>
|
||||
</bit-tab>
|
||||
|
||||
<bit-form-field *ngIf="addNewProject == true">
|
||||
<bit-label>{{ "projectName" | i18n }}</bit-label>
|
||||
<input formControlName="newProjectName" bitInput />
|
||||
</bit-form-field>
|
||||
<bit-tab label="{{ 'machineAccounts' | i18n }}">
|
||||
<p>
|
||||
{{ "secretMachineAccountsDescription" | i18n }}
|
||||
</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>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton buttonType="primary" bitFormButton>
|
||||
<button [loading]="loading" type="submit" bitButton buttonType="primary" bitFormButton>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="secondary"
|
||||
bitFormButton
|
||||
bitDialogClose
|
||||
[disabled]="false"
|
||||
>
|
||||
<button bitButton buttonType="secondary" type="button" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="deleteButtonIsVisible"
|
||||
class="tw-ml-auto"
|
||||
[disabled]="loading"
|
||||
type="button"
|
||||
bitIconButton="bwi-trash"
|
||||
buttonType="danger"
|
||||
bitIconButton="bwi-trash"
|
||||
bitFormButton
|
||||
(click)="openDeleteSecretDialog()"
|
||||
[appA11yTitle]="'delete' | i18n"
|
||||
[bitAction]="delete"
|
||||
></button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { 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 { 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 { ProjectView } from "../../models/view/project.view";
|
||||
import { SecretListView } from "../../models/view/secret-list.view";
|
||||
import { SecretProjectView } from "../../models/view/secret-project.view";
|
||||
import { SecretView } from "../../models/view/secret.view";
|
||||
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 { SecretDeleteDialogComponent, SecretDeleteOperation } from "./secret-delete.component";
|
||||
@ -24,6 +37,12 @@ export enum OperationType {
|
||||
Edit,
|
||||
}
|
||||
|
||||
export enum SecretDialogTabType {
|
||||
NameValuePair = 0,
|
||||
People = 1,
|
||||
ServiceAccounts = 2,
|
||||
}
|
||||
|
||||
export interface SecretOperation {
|
||||
organizationId: string;
|
||||
operation: OperationType;
|
||||
@ -36,6 +55,12 @@ export interface SecretOperation {
|
||||
templateUrl: "./secret-dialog.component.html",
|
||||
})
|
||||
export class SecretDialogComponent implements OnInit {
|
||||
loading = true;
|
||||
projects: ProjectListView[];
|
||||
addNewProject = false;
|
||||
newProjectGuid = Utils.newGuid();
|
||||
tabIndex: SecretDialogTabType = SecretDialogTabType.NameValuePair;
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
name: new FormControl("", {
|
||||
validators: [Validators.required, Validators.maxLength(500), BitValidators.trimValidator],
|
||||
@ -51,77 +76,60 @@ export class SecretDialogComponent implements OnInit {
|
||||
validators: [Validators.maxLength(500), BitValidators.trimValidator],
|
||||
updateOn: "submit",
|
||||
}),
|
||||
peopleAccessPolicies: new FormControl([] as ApItemValueType[]),
|
||||
serviceAccountAccessPolicies: new FormControl([] as ApItemValueType[]),
|
||||
});
|
||||
protected peopleAccessPolicyItems: ApItemViewType[];
|
||||
protected serviceAccountAccessPolicyItems: ApItemViewType[];
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private loading = true;
|
||||
projects: ProjectListView[];
|
||||
addNewProject = false;
|
||||
newProjectGuid = Utils.newGuid();
|
||||
private currentPeopleAccessPolicies: ApItemViewType[];
|
||||
|
||||
constructor(
|
||||
public dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) private data: SecretOperation,
|
||||
private secretService: SecretService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private projectService: ProjectService,
|
||||
private dialogService: DialogService,
|
||||
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() {
|
||||
this.loading = true;
|
||||
if (this.data.operation === OperationType.Edit && this.data.secretId) {
|
||||
await this.loadData();
|
||||
await this.loadEditDialog();
|
||||
} else if (this.data.operation !== OperationType.Add) {
|
||||
this.dialogRef.close();
|
||||
throw new Error(`The secret dialog was not called with the appropriate operation values.`);
|
||||
} else if (this.data.operation == OperationType.Add) {
|
||||
await this.loadProjects(true);
|
||||
if (this.data.projectId == null || this.data.projectId == "") {
|
||||
this.addNewProjectOptionToProjectsDropDown();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.data.projectId) {
|
||||
this.formGroup.get("project").setValue(this.data.projectId);
|
||||
} else if (this.data.operation === OperationType.Add) {
|
||||
await this.loadAddDialog();
|
||||
}
|
||||
|
||||
if ((await this.organizationService.get(this.data.organizationId))?.isAdmin) {
|
||||
this.formGroup.get("project").removeValidators(Validators.required);
|
||||
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;
|
||||
|
||||
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 {
|
||||
@ -129,6 +137,152 @@ export class SecretDialogComponent implements OnInit {
|
||||
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() {
|
||||
this.formGroup
|
||||
.get("project")
|
||||
@ -155,70 +309,15 @@ export class SecretDialogComponent implements OnInit {
|
||||
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) {
|
||||
return await this.projectService.create(this.data.organizationId, projectView);
|
||||
}
|
||||
|
||||
protected async openDeleteSecretDialog() {
|
||||
const secretListView: SecretListView[] = this.getSecretListView();
|
||||
|
||||
const dialogRef = this.dialogService.open<unknown, SecretDeleteOperation>(
|
||||
SecretDeleteDialogComponent,
|
||||
{
|
||||
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);
|
||||
private async createSecret(
|
||||
secretView: SecretView,
|
||||
secretAccessPoliciesView: SecretAccessPoliciesView,
|
||||
) {
|
||||
await this.secretService.create(this.data.organizationId, secretView, secretAccessPoliciesView);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("secretCreated"));
|
||||
}
|
||||
|
||||
@ -229,8 +328,11 @@ export class SecretDialogComponent implements OnInit {
|
||||
return projectView;
|
||||
}
|
||||
|
||||
private async updateSecret(secretView: SecretView) {
|
||||
await this.secretService.update(this.data.organizationId, secretView);
|
||||
private async updateSecret(
|
||||
secretView: SecretView,
|
||||
secretAccessPoliciesView: SecretAccessPoliciesView,
|
||||
) {
|
||||
await this.secretService.update(this.data.organizationId, secretView, secretAccessPoliciesView);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("secretEdited"));
|
||||
}
|
||||
|
||||
@ -265,4 +367,63 @@ export class SecretDialogComponent implements OnInit {
|
||||
secretListViews.push(secretListView);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { SecretAccessPoliciesRequest } from "../../shared/access-policies/models/requests/secret-access-policies.request";
|
||||
|
||||
export class SecretRequest {
|
||||
key: string;
|
||||
value: string;
|
||||
note: string;
|
||||
projectIds?: string[];
|
||||
accessPoliciesRequests: SecretAccessPoliciesRequest;
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
@ -7,9 +7,11 @@ import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
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 { SecretProjectView } from "../models/view/secret-project.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 { SecretRequest } from "./requests/secret.request";
|
||||
@ -30,6 +32,7 @@ export class SecretService {
|
||||
private cryptoService: CryptoService,
|
||||
private apiService: ApiService,
|
||||
private encryptService: EncryptService,
|
||||
private accessPolicyService: AccessPolicyService,
|
||||
) {}
|
||||
|
||||
async getBySecretId(secretId: string): Promise<SecretView> {
|
||||
@ -65,8 +68,16 @@ export class SecretService {
|
||||
return await this.createSecretsListView(organizationId, results);
|
||||
}
|
||||
|
||||
async create(organizationId: string, secretView: SecretView) {
|
||||
const request = await this.getSecretRequest(organizationId, secretView);
|
||||
async create(
|
||||
organizationId: string,
|
||||
secretView: SecretView,
|
||||
secretAccessPoliciesView: SecretAccessPoliciesView,
|
||||
) {
|
||||
const request = await this.getSecretRequest(
|
||||
organizationId,
|
||||
secretView,
|
||||
secretAccessPoliciesView,
|
||||
);
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
"/organizations/" + organizationId + "/secrets",
|
||||
@ -77,8 +88,16 @@ export class SecretService {
|
||||
this._secret.next(await this.createSecretView(new SecretResponse(r)));
|
||||
}
|
||||
|
||||
async update(organizationId: string, secretView: SecretView) {
|
||||
const request = await this.getSecretRequest(organizationId, secretView);
|
||||
async update(
|
||||
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);
|
||||
this._secret.next(await this.createSecretView(new SecretResponse(r)));
|
||||
}
|
||||
@ -140,6 +159,7 @@ export class SecretService {
|
||||
private async getSecretRequest(
|
||||
organizationId: string,
|
||||
secretView: SecretView,
|
||||
secretAccessPoliciesView: SecretAccessPoliciesView,
|
||||
): Promise<SecretRequest> {
|
||||
const orgKey = await this.getOrganizationKey(organizationId);
|
||||
const request = new SecretRequest();
|
||||
@ -155,6 +175,9 @@ export class SecretService {
|
||||
|
||||
secretView.projects?.forEach((e) => request.projectIds.push(e.id));
|
||||
|
||||
request.accessPoliciesRequests =
|
||||
this.accessPolicyService.getSecretAccessPoliciesRequest(secretAccessPoliciesView);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
|
@ -64,10 +64,12 @@ describe("AccessPolicySelectorService", () => {
|
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [];
|
||||
selectedPolicyValues.push(
|
||||
createApItemValueType({
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||
@ -80,15 +82,17 @@ describe("AccessPolicySelectorService", () => {
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
},
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
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 () => {
|
||||
@ -96,12 +100,15 @@ describe("AccessPolicySelectorService", () => {
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUserInGroup: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||
@ -114,12 +121,15 @@ describe("AccessPolicySelectorService", () => {
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
currentUserInGroup: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||
@ -132,12 +142,15 @@ describe("AccessPolicySelectorService", () => {
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
currentUserInGroup: false,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||
@ -150,16 +163,21 @@ describe("AccessPolicySelectorService", () => {
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType({
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
currentUserInGroup: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||
@ -172,16 +190,21 @@ describe("AccessPolicySelectorService", () => {
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType({
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
currentUserInGroup: false,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanReadWrite,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||
@ -194,16 +217,21 @@ describe("AccessPolicySelectorService", () => {
|
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType({
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUserInGroup: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "groupId",
|
||||
type: ApItemEnum.Group,
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
const result = await sut.showAccessRemovalWarning(org.id, selectedPolicyValues);
|
||||
@ -211,6 +239,246 @@ describe("AccessPolicySelectorService", () => {
|
||||
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", () => {
|
||||
it("returns false when there are no previous policies and no selected policies", async () => {
|
||||
const currentAccessPolicies: ApItemViewType[] = [];
|
||||
@ -223,11 +491,13 @@ describe("AccessPolicySelectorService", () => {
|
||||
it("returns false when there are no previous policies", async () => {
|
||||
const currentAccessPolicies: ApItemViewType[] = [];
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
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 () => {
|
||||
const currentAccessPolicies: ApItemViewType[] = [
|
||||
createApItemViewType({
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemViewType(
|
||||
{
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
];
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues);
|
||||
@ -256,23 +530,29 @@ describe("AccessPolicySelectorService", () => {
|
||||
});
|
||||
it("returns false when previous policies are still selected", async () => {
|
||||
const currentAccessPolicies: ApItemViewType[] = [
|
||||
createApItemViewType({
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemViewType(
|
||||
{
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
];
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType({
|
||||
id: "example-2",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "example-2",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
const result = sut.isAccessRemoval(currentAccessPolicies, selectedPolicyValues);
|
||||
@ -281,23 +561,29 @@ describe("AccessPolicySelectorService", () => {
|
||||
});
|
||||
it("returns true when previous policies are not selected", async () => {
|
||||
const currentAccessPolicies: ApItemViewType[] = [
|
||||
createApItemViewType({
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemViewType(
|
||||
{
|
||||
id: "example",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
];
|
||||
const selectedPolicyValues: ApItemValueType[] = [
|
||||
createApItemValueType({
|
||||
id: "test",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType({
|
||||
id: "example-2",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "test",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
createApItemValueType(
|
||||
{
|
||||
id: "example-2",
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
];
|
||||
|
||||
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 () => {
|
||||
const currentAccessPolicies: ApItemViewType[] = [
|
||||
createApItemViewType({
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
currentUser: true,
|
||||
}),
|
||||
createApItemViewType(
|
||||
{
|
||||
permission: ApPermissionEnum.CanRead,
|
||||
},
|
||||
true,
|
||||
),
|
||||
];
|
||||
const selectedPolicyValues: ApItemValueType[] = [];
|
||||
|
||||
@ -331,17 +619,31 @@ const orgFactory = (props: Partial<Organization> = {}) =>
|
||||
props,
|
||||
);
|
||||
|
||||
function createApItemValueType(options: Partial<ApItemValueType> = {}) {
|
||||
return {
|
||||
function createApItemValueType(
|
||||
options: Partial<ApItemValueType> = {},
|
||||
currentUser = false,
|
||||
currentUserInGroup = false,
|
||||
) {
|
||||
const item: ApItemValueType = {
|
||||
id: options?.id ?? "test",
|
||||
type: options?.type ?? ApItemEnum.User,
|
||||
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> = {}) {
|
||||
return {
|
||||
function createApItemViewType(
|
||||
options: Partial<ApItemViewType> = {},
|
||||
currentUser = false,
|
||||
currentUserInGroup = false,
|
||||
) {
|
||||
const item: ApItemViewType = {
|
||||
id: options?.id ?? "test",
|
||||
listName: options?.listName ?? "test",
|
||||
labelName: options?.labelName ?? "test",
|
||||
@ -349,6 +651,13 @@ function createApItemViewType(options: Partial<ApItemViewType> = {}) {
|
||||
permission: options?.permission ?? ApPermissionEnum.CanRead,
|
||||
readOnly: options?.readOnly ?? false,
|
||||
};
|
||||
if (item.type === ApItemEnum.User) {
|
||||
item.currentUser = currentUser;
|
||||
}
|
||||
if (item.type === ApItemEnum.Group) {
|
||||
item.currentUserInGroup = currentUserInGroup;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function setupUserOrg() {
|
||||
|
@ -22,26 +22,29 @@ export class AccessPolicySelectorService {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedUserReadWritePolicy = selectedPoliciesValues.find(
|
||||
(s) =>
|
||||
s.type === ApItemEnum.User &&
|
||||
s.currentUser &&
|
||||
s.permission === ApPermissionEnum.CanReadWrite,
|
||||
);
|
||||
if (!this.userHasReadWriteAccess(selectedPoliciesValues)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const selectedGroupReadWritePolicies = selectedPoliciesValues.filter(
|
||||
(s) =>
|
||||
s.type === ApItemEnum.Group &&
|
||||
s.permission == ApPermissionEnum.CanReadWrite &&
|
||||
s.currentUserInGroup,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedGroupReadWritePolicies == null || selectedGroupReadWritePolicies.length == 0) {
|
||||
if (selectedUserReadWritePolicy == null) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
async showSecretAccessRemovalWarning(
|
||||
organizationId: string,
|
||||
current: ApItemViewType[],
|
||||
selectedPoliciesValues: ApItemValueType[],
|
||||
): 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;
|
||||
@ -67,4 +70,25 @@ export class AccessPolicySelectorService {
|
||||
const selectedIds = selected.map((x) => x.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;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/
|
||||
|
||||
import { AccessPolicyRequest } from "./models/requests/access-policy.request";
|
||||
import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request";
|
||||
import { SecretAccessPoliciesRequest } from "./models/requests/secret-access-policies.request";
|
||||
import {
|
||||
GroupAccessPolicyResponse,
|
||||
UserAccessPolicyResponse,
|
||||
@ -233,6 +234,20 @@ export class AccessPolicyService {
|
||||
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> {
|
||||
return await this.cryptoService.getOrgKey(organizationId);
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { AccessPolicyRequest } from "./access-policy.request";
|
||||
|
||||
export class SecretAccessPoliciesRequest {
|
||||
userAccessPolicyRequests: AccessPolicyRequest[];
|
||||
groupAccessPolicyRequests: AccessPolicyRequest[];
|
||||
serviceAccountAccessPolicyRequests: AccessPolicyRequest[];
|
||||
}
|
Loading…
Reference in New Issue
Block a user