mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[SM-670] Restrict UI actions based on user permission (#5090)
* Restrict UI actions based on user permission * Swap to hiding bulk option without permission * Fix read/write assignment in project service * Filter projects based on permission in dialog * Fix encryption error for updating secret result * Fix spinner (#5182) * Swap to bit-no-items * [SM-699] Projects bulk delete - add bulk confirmation dialog (#5200) * Add bulk confirmation dialog * Code review updates * Code review - load projects * code review - swap to observable * Code review - remove oninit
This commit is contained in:
parent
95b1ea318c
commit
208e3f30b4
@ -1264,7 +1264,7 @@
|
|||||||
"example": "2"
|
"example": "2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dataExportSuccess": {
|
"dataExportSuccess": {
|
||||||
"message": "Data successfully exported"
|
"message": "Data successfully exported"
|
||||||
},
|
},
|
||||||
@ -6736,7 +6736,19 @@
|
|||||||
"notAvailableForFreeOrganization": {
|
"notAvailableForFreeOrganization": {
|
||||||
"message": "This feature is not available for free organizations. Contact your organization owner to upgrade."
|
"message": "This feature is not available for free organizations. Contact your organization owner to upgrade."
|
||||||
},
|
},
|
||||||
|
"smProjectSecretsNoItemsNoAccess": {
|
||||||
|
"message": "Contact your organization's admin to manage secrets for this project.",
|
||||||
|
"description": "The message shown to the user under a project's secrets tab when the user only has read access to the project."
|
||||||
|
},
|
||||||
"enforceOnLoginDesc": {
|
"enforceOnLoginDesc": {
|
||||||
"message": "Require existing members to change their passwords"
|
"message": "Require existing members to change their passwords"
|
||||||
|
},
|
||||||
|
"smProjectDeleteAccessRestricted": {
|
||||||
|
"message": "You don't have permissions to delete this project",
|
||||||
|
"description": "The individual description shown to the user when the user doesn't have access to delete a project."
|
||||||
|
},
|
||||||
|
"smProjectsDeleteBulkConfirmation": {
|
||||||
|
"message": "The following projects can not be deleted. Would you like to continue?",
|
||||||
|
"description": "The message shown to the user when bulk deleting projects and the user doesn't have access to some projects."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,4 +4,6 @@ export class ProjectListView {
|
|||||||
name: string;
|
name: string;
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
revisionDate: string;
|
revisionDate: string;
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,6 @@ export class ProjectView {
|
|||||||
name: string;
|
name: string;
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
revisionDate: string;
|
revisionDate: string;
|
||||||
}
|
|
||||||
|
|
||||||
export class ProjectPermissionDetailsView extends ProjectView {
|
|
||||||
read: boolean;
|
read: boolean;
|
||||||
write: boolean;
|
write: boolean;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ export class ProjectListItemResponse extends BaseResponse {
|
|||||||
name: string;
|
name: string;
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
revisionDate: string;
|
revisionDate: string;
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@ -14,5 +16,7 @@ export class ProjectListItemResponse extends BaseResponse {
|
|||||||
this.name = this.getResponseProperty("Name");
|
this.name = this.getResponseProperty("Name");
|
||||||
this.creationDate = this.getResponseProperty("CreationDate");
|
this.creationDate = this.getResponseProperty("CreationDate");
|
||||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||||
|
this.read = this.getResponseProperty("Read");
|
||||||
|
this.write = this.getResponseProperty("Write");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ export class ProjectResponse extends BaseResponse {
|
|||||||
name: string;
|
name: string;
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
revisionDate: string;
|
revisionDate: string;
|
||||||
|
read: boolean;
|
||||||
|
write: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@ -14,15 +16,6 @@ export class ProjectResponse extends BaseResponse {
|
|||||||
this.name = this.getResponseProperty("Name");
|
this.name = this.getResponseProperty("Name");
|
||||||
this.creationDate = this.getResponseProperty("CreationDate");
|
this.creationDate = this.getResponseProperty("CreationDate");
|
||||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ProjectPermissionDetailsResponse extends ProjectResponse {
|
|
||||||
read: boolean;
|
|
||||||
write: boolean;
|
|
||||||
|
|
||||||
constructor(response: any) {
|
|
||||||
super(response);
|
|
||||||
this.read = this.getResponseProperty("Read");
|
this.read = this.getResponseProperty("Read");
|
||||||
this.write = this.getResponseProperty("Write");
|
this.write = this.getResponseProperty("Write");
|
||||||
}
|
}
|
||||||
|
@ -9,15 +9,12 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-cr
|
|||||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
|
|
||||||
import { ProjectListView } from "../models/view/project-list.view";
|
import { ProjectListView } from "../models/view/project-list.view";
|
||||||
import { ProjectPermissionDetailsView, ProjectView } from "../models/view/project.view";
|
import { ProjectView } from "../models/view/project.view";
|
||||||
import { BulkOperationStatus } from "../shared/dialogs/bulk-status-dialog.component";
|
import { BulkOperationStatus } from "../shared/dialogs/bulk-status-dialog.component";
|
||||||
|
|
||||||
import { ProjectRequest } from "./models/requests/project.request";
|
import { ProjectRequest } from "./models/requests/project.request";
|
||||||
import { ProjectListItemResponse } from "./models/responses/project-list-item.response";
|
import { ProjectListItemResponse } from "./models/responses/project-list-item.response";
|
||||||
import {
|
import { ProjectResponse } from "./models/responses/project.response";
|
||||||
ProjectPermissionDetailsResponse,
|
|
||||||
ProjectResponse,
|
|
||||||
} from "./models/responses/project.response";
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: "root",
|
providedIn: "root",
|
||||||
@ -32,10 +29,10 @@ export class ProjectService {
|
|||||||
private encryptService: EncryptService
|
private encryptService: EncryptService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getByProjectId(projectId: string): Promise<ProjectPermissionDetailsView> {
|
async getByProjectId(projectId: string): Promise<ProjectView> {
|
||||||
const r = await this.apiService.send("GET", "/projects/" + projectId, null, true, true);
|
const r = await this.apiService.send("GET", "/projects/" + projectId, null, true, true);
|
||||||
const projectResponse = new ProjectPermissionDetailsResponse(r);
|
const projectResponse = new ProjectResponse(r);
|
||||||
return await this.createProjectPermissionDetailsView(projectResponse);
|
return await this.createProjectView(projectResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(organizationId: string): Promise<ProjectListView[]> {
|
async getProjects(organizationId: string): Promise<ProjectListView[]> {
|
||||||
@ -99,9 +96,7 @@ export class ProjectService {
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createProjectView(
|
private async createProjectView(projectResponse: ProjectResponse) {
|
||||||
projectResponse: ProjectResponse | ProjectPermissionDetailsResponse
|
|
||||||
) {
|
|
||||||
const orgKey = await this.getOrganizationKey(projectResponse.organizationId);
|
const orgKey = await this.getOrganizationKey(projectResponse.organizationId);
|
||||||
|
|
||||||
const projectView = new ProjectView();
|
const projectView = new ProjectView();
|
||||||
@ -109,6 +104,8 @@ export class ProjectService {
|
|||||||
projectView.organizationId = projectResponse.organizationId;
|
projectView.organizationId = projectResponse.organizationId;
|
||||||
projectView.creationDate = projectResponse.creationDate;
|
projectView.creationDate = projectResponse.creationDate;
|
||||||
projectView.revisionDate = projectResponse.revisionDate;
|
projectView.revisionDate = projectResponse.revisionDate;
|
||||||
|
projectView.read = projectResponse.read;
|
||||||
|
projectView.write = projectResponse.write;
|
||||||
projectView.name = await this.encryptService.decryptToUtf8(
|
projectView.name = await this.encryptService.decryptToUtf8(
|
||||||
new EncString(projectResponse.name),
|
new EncString(projectResponse.name),
|
||||||
orgKey
|
orgKey
|
||||||
@ -116,16 +113,6 @@ export class ProjectService {
|
|||||||
return projectView;
|
return projectView;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createProjectPermissionDetailsView(
|
|
||||||
projectResponse: ProjectPermissionDetailsResponse
|
|
||||||
): Promise<ProjectPermissionDetailsView> {
|
|
||||||
return {
|
|
||||||
...(await this.createProjectView(projectResponse)),
|
|
||||||
read: projectResponse.read,
|
|
||||||
write: projectResponse.write,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createProjectsListView(
|
private async createProjectsListView(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
projects: ProjectListItemResponse[]
|
projects: ProjectListItemResponse[]
|
||||||
@ -136,6 +123,8 @@ export class ProjectService {
|
|||||||
const projectListView = new ProjectListView();
|
const projectListView = new ProjectListView();
|
||||||
projectListView.id = s.id;
|
projectListView.id = s.id;
|
||||||
projectListView.organizationId = s.organizationId;
|
projectListView.organizationId = s.organizationId;
|
||||||
|
projectListView.read = s.read;
|
||||||
|
projectListView.write = s.write;
|
||||||
projectListView.name = await this.encryptService.decryptToUtf8(
|
projectListView.name = await this.encryptService.decryptToUtf8(
|
||||||
new EncString(s.name),
|
new EncString(s.name),
|
||||||
orgKey
|
orgKey
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
<ng-container *ngIf="secrets$ | async as secrets; else spinner">
|
<ng-container *ngIf="{ project: project$ | async, secrets: secrets$ | async } as projectSecrets">
|
||||||
<div *ngIf="secrets.length > 0" class="float-right tw-mt-3 tw-items-center">
|
<ng-container *ngIf="projectSecrets?.secrets && projectSecrets?.project; else spinner">
|
||||||
<button type="button" bitButton buttonType="secondary" (click)="openNewSecretDialog()">
|
<div
|
||||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
*ngIf="projectSecrets.secrets?.length > 0 && projectSecrets.project?.write"
|
||||||
{{ "newSecret" | i18n }}
|
class="float-right tw-mt-3 tw-items-center"
|
||||||
</button>
|
>
|
||||||
</div>
|
<button type="button" bitButton buttonType="secondary" (click)="openNewSecretDialog()">
|
||||||
<sm-secrets-list
|
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||||
(deleteSecretsEvent)="openDeleteSecret($event)"
|
{{ "newSecret" | i18n }}
|
||||||
(newSecretEvent)="openNewSecretDialog()"
|
</button>
|
||||||
(editSecretEvent)="openEditSecret($event)"
|
</div>
|
||||||
(copySecretNameEvent)="copySecretName($event)"
|
<sm-secrets-list
|
||||||
(copySecretValueEvent)="copySecretValue($event)"
|
*ngIf="projectSecrets.secrets?.length > 0 || projectSecrets.project?.write; else contactAdmin"
|
||||||
[secrets]="secrets"
|
(deleteSecretsEvent)="openDeleteSecret($event)"
|
||||||
></sm-secrets-list>
|
(newSecretEvent)="openNewSecretDialog()"
|
||||||
|
(editSecretEvent)="openEditSecret($event)"
|
||||||
|
(copySecretNameEvent)="copySecretName($event)"
|
||||||
|
(copySecretValueEvent)="copySecretValue($event)"
|
||||||
|
[secrets]="projectSecrets.secrets"
|
||||||
|
></sm-secrets-list>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #spinner>
|
<ng-template #spinner>
|
||||||
@ -20,3 +26,10 @@
|
|||||||
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #contactAdmin>
|
||||||
|
<bit-no-items>
|
||||||
|
<ng-container slot="title">{{ "secretsNoItemsTitle" | i18n }}</ng-container>
|
||||||
|
<ng-container slot="description">{{ "smProjectSecretsNoItemsNoAccess" | i18n }}</ng-container>
|
||||||
|
</bit-no-items>
|
||||||
|
</ng-template>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs";
|
import { combineLatest, combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { ProjectView } from "../../models/view/project.view";
|
||||||
import { SecretListView } from "../../models/view/secret-list.view";
|
import { SecretListView } from "../../models/view/secret-list.view";
|
||||||
import {
|
import {
|
||||||
SecretDeleteDialogComponent,
|
SecretDeleteDialogComponent,
|
||||||
@ -29,6 +30,7 @@ export class ProjectSecretsComponent {
|
|||||||
|
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
private projectId: string;
|
private projectId: string;
|
||||||
|
protected project$: Observable<ProjectView>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@ -46,6 +48,12 @@ export class ProjectSecretsComponent {
|
|||||||
startWith(null)
|
startWith(null)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.project$ = combineLatest([this.route.params, currentProjectEdited]).pipe(
|
||||||
|
switchMap(([params, _]) => {
|
||||||
|
return this.projectService.getByProjectId(params.projectId);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
this.secrets$ = this.secretService.secret$.pipe(
|
this.secrets$ = this.secretService.secret$.pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
combineLatestWith(this.route.params, currentProjectEdited),
|
combineLatestWith(this.route.params, currentProjectEdited),
|
||||||
|
@ -4,7 +4,7 @@ import { combineLatest, filter, Observable, startWith, Subject, switchMap, takeU
|
|||||||
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { ProjectPermissionDetailsView } from "../../models/view/project.view";
|
import { ProjectView } from "../../models/view/project.view";
|
||||||
import {
|
import {
|
||||||
OperationType,
|
OperationType,
|
||||||
ProjectDialogComponent,
|
ProjectDialogComponent,
|
||||||
@ -17,7 +17,7 @@ import { ProjectService } from "../project.service";
|
|||||||
templateUrl: "./project.component.html",
|
templateUrl: "./project.component.html",
|
||||||
})
|
})
|
||||||
export class ProjectComponent implements OnInit, OnDestroy {
|
export class ProjectComponent implements OnInit, OnDestroy {
|
||||||
protected project$: Observable<ProjectPermissionDetailsView>;
|
protected project$: Observable<ProjectView>;
|
||||||
|
|
||||||
private organizationId: string;
|
private organizationId: string;
|
||||||
private projectId: string;
|
private projectId: string;
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { combineLatest, Observable, startWith, switchMap } from "rxjs";
|
import { combineLatest, lastValueFrom, Observable, startWith, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { ProjectListView } from "../../models/view/project-list.view";
|
import { ProjectListView } from "../../models/view/project-list.view";
|
||||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||||
|
import {
|
||||||
|
BulkConfirmationDetails,
|
||||||
|
BulkConfirmationDialogComponent,
|
||||||
|
BulkConfirmationResult,
|
||||||
|
BulkConfirmationStatus,
|
||||||
|
} from "../../shared/dialogs/bulk-confirmation-dialog.component";
|
||||||
import {
|
import {
|
||||||
ProjectDeleteDialogComponent,
|
ProjectDeleteDialogComponent,
|
||||||
ProjectDeleteOperation,
|
ProjectDeleteOperation,
|
||||||
@ -70,11 +76,48 @@ export class ProjectsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openDeleteProjectDialog(event: ProjectListView[]) {
|
async openDeleteProjectDialog(projects: ProjectListView[]) {
|
||||||
this.dialogService.open<unknown, ProjectDeleteOperation>(ProjectDeleteDialogComponent, {
|
if (projects.some((project) => project.write == false)) {
|
||||||
data: {
|
const readOnlyProjects = projects.filter((project) => project.write == false);
|
||||||
projects: event,
|
const writeProjects = projects.filter((project) => project.write);
|
||||||
},
|
|
||||||
|
const dialogRef = this.dialogService.open<unknown, BulkConfirmationDetails>(
|
||||||
|
BulkConfirmationDialogComponent,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
title: "deleteProjects",
|
||||||
|
columnTitle: "projectName",
|
||||||
|
message: "smProjectsDeleteBulkConfirmation",
|
||||||
|
details: this.getBulkConfirmationDetails(readOnlyProjects),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await lastValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
if (result == BulkConfirmationResult.Continue) {
|
||||||
|
this.dialogService.open<unknown, ProjectDeleteOperation>(ProjectDeleteDialogComponent, {
|
||||||
|
data: {
|
||||||
|
projects: writeProjects,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.dialogService.open<unknown, ProjectDeleteOperation>(ProjectDeleteDialogComponent, {
|
||||||
|
data: {
|
||||||
|
projects,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBulkConfirmationDetails(projects: ProjectListView[]): BulkConfirmationStatus[] {
|
||||||
|
return projects.map((project) => {
|
||||||
|
return {
|
||||||
|
id: project.id,
|
||||||
|
name: project.name,
|
||||||
|
description: "smProjectDeleteAccessRestricted",
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,8 @@ export class SecretDialogComponent implements OnInit {
|
|||||||
} 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) {
|
||||||
|
await this.loadProjects(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.data.projectId) {
|
if (this.data.projectId) {
|
||||||
@ -72,15 +74,14 @@ export class SecretDialogComponent implements OnInit {
|
|||||||
this.formGroup.get("project").removeValidators(Validators.required);
|
this.formGroup.get("project").removeValidators(Validators.required);
|
||||||
this.formGroup.get("project").updateValueAndValidity();
|
this.formGroup.get("project").updateValueAndValidity();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.projects = await this.projectService
|
|
||||||
.getProjects(this.data.organizationId)
|
|
||||||
.then((projects) => projects.sort((a, b) => a.name.localeCompare(b.name)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadData() {
|
async loadData() {
|
||||||
this.formGroup.disable();
|
this.formGroup.disable();
|
||||||
const secret: SecretView = await this.secretService.getBySecretId(this.data.secretId);
|
const secret: SecretView = await this.secretService.getBySecretId(this.data.secretId);
|
||||||
|
|
||||||
|
await this.loadProjects(secret.write);
|
||||||
|
|
||||||
this.formGroup.setValue({
|
this.formGroup.setValue({
|
||||||
name: secret.name,
|
name: secret.name,
|
||||||
value: secret.value,
|
value: secret.value,
|
||||||
@ -95,6 +96,16 @@ export class SecretDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
|
@ -233,10 +233,9 @@ export class SecretService {
|
|||||||
projects.map(async (s: SecretProjectResponse) => {
|
projects.map(async (s: SecretProjectResponse) => {
|
||||||
const projectsMappedToSecretView = new SecretProjectView();
|
const projectsMappedToSecretView = new SecretProjectView();
|
||||||
projectsMappedToSecretView.id = s.id;
|
projectsMappedToSecretView.id = s.id;
|
||||||
projectsMappedToSecretView.name = await this.encryptService.decryptToUtf8(
|
projectsMappedToSecretView.name = s.name
|
||||||
new EncString(s.name),
|
? await this.encryptService.decryptToUtf8(new EncString(s.name), orgKey)
|
||||||
orgKey
|
: null;
|
||||||
);
|
|
||||||
return projectsMappedToSecretView;
|
return projectsMappedToSecretView;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -50,16 +50,21 @@ export class AccessRemovalDialogComponent implements OnInit {
|
|||||||
await this.accessPolicyService.updateAccessPolicy(
|
await this.accessPolicyService.updateAccessPolicy(
|
||||||
AccessSelectorComponent.getBaseAccessPolicyView(this.data.policy)
|
AccessSelectorComponent.getBaseAccessPolicyView(this.data.policy)
|
||||||
);
|
);
|
||||||
|
this.refreshPolicyChanges();
|
||||||
}
|
}
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
cancel = () => {
|
cancel = () => {
|
||||||
|
this.refreshPolicyChanges();
|
||||||
|
this.dialogRef.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
private refreshPolicyChanges() {
|
||||||
if (this.data.type == "project") {
|
if (this.data.type == "project") {
|
||||||
this.accessPolicyService.refreshProjectAccessPolicyChanges();
|
this.accessPolicyService.refreshProjectAccessPolicyChanges();
|
||||||
} else if (this.data.type == "service-account") {
|
} else if (this.data.type == "service-account") {
|
||||||
this.accessPolicyService.refreshServiceAccountAccessPolicyChanges();
|
this.accessPolicyService.refreshServiceAccountAccessPolicyChanges();
|
||||||
}
|
}
|
||||||
this.dialogRef.close();
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
<bit-dialog>
|
||||||
|
<ng-container bitDialogTitle>
|
||||||
|
{{ data.title | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div bitDialogContent>
|
||||||
|
{{ data.message | i18n }}
|
||||||
|
<bit-table>
|
||||||
|
<ng-container header>
|
||||||
|
<tr>
|
||||||
|
<th bitCell>{{ data.columnTitle | i18n }}</th>
|
||||||
|
<th bitCell>{{ "description" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template body>
|
||||||
|
<tr bitRow *ngFor="let detail of data.details">
|
||||||
|
<td bitCell>{{ detail.name }}</td>
|
||||||
|
<td bitCell>{{ detail.description | i18n }}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</bit-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div bitDialogFooter class="tw-flex tw-gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
buttonType="primary"
|
||||||
|
bitFormButton
|
||||||
|
(click)="dialogRef.close(bulkConfirmationResult.Continue)"
|
||||||
|
>
|
||||||
|
{{ "continue" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||||
|
{{ "cancel" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</bit-dialog>
|
@ -0,0 +1,48 @@
|
|||||||
|
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
import { Component, Inject, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
export interface BulkConfirmationDetails {
|
||||||
|
title: string;
|
||||||
|
columnTitle: string;
|
||||||
|
message: string;
|
||||||
|
details: BulkConfirmationStatus[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BulkConfirmationStatus {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BulkConfirmationResult {
|
||||||
|
Continue,
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "sm-bulk-confirmation-dialog",
|
||||||
|
templateUrl: "./bulk-confirmation-dialog.component.html",
|
||||||
|
})
|
||||||
|
export class BulkConfirmationDialogComponent implements OnInit {
|
||||||
|
constructor(
|
||||||
|
public dialogRef: DialogRef,
|
||||||
|
@Inject(DIALOG_DATA) public data: BulkConfirmationDetails
|
||||||
|
) {}
|
||||||
|
|
||||||
|
protected bulkConfirmationResult = BulkConfirmationResult;
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// TODO remove null checks once strictNullChecks in TypeScript is turned on.
|
||||||
|
if (
|
||||||
|
!this.data.title ||
|
||||||
|
!this.data.columnTitle ||
|
||||||
|
!this.data.message ||
|
||||||
|
!(this.data.details?.length >= 1)
|
||||||
|
) {
|
||||||
|
this.dialogRef.close();
|
||||||
|
throw new Error(
|
||||||
|
"The bulk confirmation dialog was not called with the appropriate operation values."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,7 @@
|
|||||||
<th bitCell bitSortable="revisionDate">{{ "lastEdited" | i18n }}</th>
|
<th bitCell bitSortable="revisionDate">{{ "lastEdited" | i18n }}</th>
|
||||||
<th bitCell class="tw-w-0">
|
<th bitCell class="tw-w-0">
|
||||||
<button
|
<button
|
||||||
|
*ngIf="hasWriteAccessOnSelected$ | async"
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-ellipsis-v"
|
bitIconButton="bwi-ellipsis-v"
|
||||||
buttonType="main"
|
buttonType="main"
|
||||||
@ -74,7 +75,12 @@
|
|||||||
></button>
|
></button>
|
||||||
</td>
|
</td>
|
||||||
<bit-menu #projectMenu>
|
<bit-menu #projectMenu>
|
||||||
<button type="button" bitMenuItem (click)="editProjectEvent.emit(project.id)">
|
<button
|
||||||
|
*ngIf="project.write"
|
||||||
|
type="button"
|
||||||
|
bitMenuItem
|
||||||
|
(click)="editProjectEvent.emit(project.id)"
|
||||||
|
>
|
||||||
<i class="bwi bwi-fw bwi-pencil" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-pencil" aria-hidden="true"></i>
|
||||||
{{ "editProject" | i18n }}
|
{{ "editProject" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
@ -82,7 +88,7 @@
|
|||||||
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
|
||||||
{{ "viewProject" | i18n }}
|
{{ "viewProject" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
<button type="button" bitMenuItem (click)="deleteProject(project.id)">
|
<button *ngIf="project.write" type="button" bitMenuItem (click)="deleteProject(project.id)">
|
||||||
<i class="bwi bwi-fw bwi-trash tw-text-danger" aria-hidden="true"></i>
|
<i class="bwi bwi-fw bwi-trash tw-text-danger" aria-hidden="true"></i>
|
||||||
<span class="tw-text-danger">{{ "deleteProject" | i18n }}</span>
|
<span class="tw-text-danger">{{ "deleteProject" | i18n }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { SelectionModel } from "@angular/cdk/collections";
|
import { SelectionModel } from "@angular/cdk/collections";
|
||||||
import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
import { map } from "rxjs";
|
||||||
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
@ -12,9 +12,7 @@ import { ProjectListView } from "../models/view/project-list.view";
|
|||||||
selector: "sm-projects-list",
|
selector: "sm-projects-list",
|
||||||
templateUrl: "./projects-list.component.html",
|
templateUrl: "./projects-list.component.html",
|
||||||
})
|
})
|
||||||
export class ProjectsListComponent implements OnDestroy {
|
export class ProjectsListComponent {
|
||||||
protected dataSource = new TableDataSource<ProjectListView>();
|
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
get projects(): ProjectListView[] {
|
get projects(): ProjectListView[] {
|
||||||
return this._projects;
|
return this._projects;
|
||||||
@ -33,26 +31,18 @@ export class ProjectsListComponent implements OnDestroy {
|
|||||||
|
|
||||||
@Output() editProjectEvent = new EventEmitter<string>();
|
@Output() editProjectEvent = new EventEmitter<string>();
|
||||||
@Output() deleteProjectEvent = new EventEmitter<ProjectListView[]>();
|
@Output() deleteProjectEvent = new EventEmitter<ProjectListView[]>();
|
||||||
@Output() onProjectCheckedEvent = new EventEmitter<string[]>();
|
|
||||||
@Output() newProjectEvent = new EventEmitter();
|
@Output() newProjectEvent = new EventEmitter();
|
||||||
|
|
||||||
private destroy$: Subject<void> = new Subject<void>();
|
|
||||||
|
|
||||||
selection = new SelectionModel<string>(true, []);
|
selection = new SelectionModel<string>(true, []);
|
||||||
|
protected dataSource = new TableDataSource<ProjectListView>();
|
||||||
|
protected hasWriteAccessOnSelected$ = this.selection.changed.pipe(
|
||||||
|
map((_) => this.selectedHasWriteAccess())
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService
|
private platformUtilsService: PlatformUtilsService
|
||||||
) {
|
) {}
|
||||||
this.selection.changed
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
|
||||||
.subscribe((_) => this.onProjectCheckedEvent.emit(this.selection.selected));
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
isAllSelected() {
|
isAllSelected() {
|
||||||
const numSelected = this.selection.selected.length;
|
const numSelected = this.selection.selected.length;
|
||||||
@ -83,4 +73,14 @@ export class ProjectsListComponent implements OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private selectedHasWriteAccess() {
|
||||||
|
const selectedProjects = this.projects.filter((project) =>
|
||||||
|
this.selection.isSelected(project.id)
|
||||||
|
);
|
||||||
|
if (selectedProjects.some((project) => project.write)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
|||||||
|
|
||||||
import { AccessSelectorComponent } from "./access-policies/access-selector.component";
|
import { AccessSelectorComponent } from "./access-policies/access-selector.component";
|
||||||
import { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-removal-dialog.component";
|
import { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-removal-dialog.component";
|
||||||
|
import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component";
|
||||||
import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component";
|
import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component";
|
||||||
import { HeaderComponent } from "./header.component";
|
import { HeaderComponent } from "./header.component";
|
||||||
import { NewMenuComponent } from "./new-menu.component";
|
import { NewMenuComponent } from "./new-menu.component";
|
||||||
@ -30,6 +31,7 @@ import { SecretsListComponent } from "./secrets-list.component";
|
|||||||
AccessRemovalDialogComponent,
|
AccessRemovalDialogComponent,
|
||||||
AccessSelectorComponent,
|
AccessSelectorComponent,
|
||||||
BulkStatusDialogComponent,
|
BulkStatusDialogComponent,
|
||||||
|
BulkConfirmationDialogComponent,
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
NewMenuComponent,
|
NewMenuComponent,
|
||||||
ProjectsListComponent,
|
ProjectsListComponent,
|
||||||
@ -40,6 +42,7 @@ import { SecretsListComponent } from "./secrets-list.component";
|
|||||||
declarations: [
|
declarations: [
|
||||||
AccessRemovalDialogComponent,
|
AccessRemovalDialogComponent,
|
||||||
BulkStatusDialogComponent,
|
BulkStatusDialogComponent,
|
||||||
|
BulkConfirmationDialogComponent,
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
NewMenuComponent,
|
NewMenuComponent,
|
||||||
ProjectsListComponent,
|
ProjectsListComponent,
|
||||||
|
Loading…
Reference in New Issue
Block a user