1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-23 03:33:54 +00:00

Making accessing a project only use one api call to getbyprojectid (#15854)

This commit is contained in:
cd-bitwarden
2025-08-27 13:51:02 -04:00
committed by GitHub
parent 4dd7e0cafa
commit 1d5115f190
6 changed files with 12 additions and 201 deletions

View File

@@ -1,135 +0,0 @@
import { Component } from "@angular/core";
import { TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { MockProxy, mock } from "jest-mock-extended";
import { of } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { RouterService } from "@bitwarden/web-vault/app/core";
import { ProjectView } from "../../models/view/project.view";
import { ProjectService } from "../project.service";
import { projectAccessGuard } from "./project-access.guard";
@Component({
template: "",
standalone: false,
})
export class GuardedRouteTestComponent {}
@Component({
template: "",
standalone: false,
})
export class RedirectTestComponent {}
describe("Project Redirect Guard", () => {
let organizationService: MockProxy<OrganizationService>;
let routerService: MockProxy<RouterService>;
let projectServiceMock: MockProxy<ProjectService>;
let i18nServiceMock: MockProxy<I18nService>;
let toastService: MockProxy<ToastService>;
let router: Router;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId;
const smOrg1 = { id: "123", canAccessSecretsManager: true } as Organization;
const projectView = {
id: "123",
organizationId: "123",
name: "project-name",
creationDate: Date.now.toString(),
revisionDate: Date.now.toString(),
read: true,
write: true,
} as ProjectView;
beforeEach(async () => {
organizationService = mock<OrganizationService>();
routerService = mock<RouterService>();
projectServiceMock = mock<ProjectService>();
i18nServiceMock = mock<I18nService>();
toastService = mock<ToastService>();
accountService = mockAccountServiceWith(userId);
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{
path: "sm/:organizationId/projects/:projectId",
component: GuardedRouteTestComponent,
canActivate: [projectAccessGuard],
},
{
path: "sm",
component: RedirectTestComponent,
},
{
path: "sm/:organizationId/projects",
component: RedirectTestComponent,
},
]),
],
providers: [
{ provide: OrganizationService, useValue: organizationService },
{ provide: AccountService, useValue: accountService },
{ provide: RouterService, useValue: routerService },
{ provide: ProjectService, useValue: projectServiceMock },
{ provide: I18nService, useValue: i18nServiceMock },
{ provide: ToastService, useValue: toastService },
],
});
router = TestBed.inject(Router);
});
it("redirects to sm/{orgId}/projects/{projectId} if project exists", async () => {
// Arrange
organizationService.organizations$.mockReturnValue(of([smOrg1]));
projectServiceMock.getByProjectId.mockReturnValue(Promise.resolve(projectView));
// Act
await router.navigateByUrl("sm/123/projects/123");
// Assert
expect(router.url).toBe("/sm/123/projects/123");
});
it("redirects to sm/projects if project does not exist", async () => {
// Arrange
organizationService.organizations$.mockReturnValue(of([smOrg1]));
// Act
await router.navigateByUrl("sm/123/projects/124");
// Assert
expect(router.url).toBe("/sm/123/projects");
});
it("redirects to sm/123/projects if exception occurs while looking for Project", async () => {
// Arrange
jest.spyOn(projectServiceMock, "getByProjectId").mockImplementation(() => {
throw new Error("Test error");
});
jest.spyOn(i18nServiceMock, "t").mockReturnValue("Project not found");
// Act
await router.navigateByUrl("sm/123/projects/123");
// Assert
expect(toastService.showToast).toHaveBeenCalledWith({
variant: "error",
title: null,
message: "Project not found",
});
expect(router.url).toBe("/sm/123/projects");
});
});

View File

@@ -1,33 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { ToastService } from "@bitwarden/components";
import { ProjectService } from "../project.service";
/**
* Redirects to projects list if the user doesn't have access to project.
*/
export const projectAccessGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
const projectService = inject(ProjectService);
const toastService = inject(ToastService);
const i18nService = inject(I18nService);
try {
const project = await projectService.getByProjectId(route.params.projectId);
if (project) {
return true;
}
} catch {
toastService.showToast({
variant: "error",
title: null,
message: i18nService.t("notFound", i18nService.t("project")),
});
return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "projects"]);
}
return createUrlTreeFromSnapshot(route, ["/sm", route.params.organizationId, "projects"]);
};

View File

@@ -1,7 +1,7 @@
<ng-container *ngIf="{ project: project$ | async, secrets: secrets$ | async } as projectSecrets">
<ng-container *ngIf="projectSecrets?.secrets && projectSecrets?.project; else spinner">
<ng-container *ngIf="{ secrets: secrets$ | async } as projectSecrets">
<ng-container *ngIf="projectSecrets?.secrets && this.projectExists(); else spinner">
<div
*ngIf="projectSecrets.secrets?.length > 0 && projectSecrets.project?.write"
*ngIf="projectSecrets.secrets?.length > 0 && this.writeAccess()"
class="tw-float-right tw-mt-3 tw-items-center"
>
<button type="button" bitButton buttonType="secondary" (click)="openNewSecretDialog()">
@@ -10,7 +10,7 @@
</button>
</div>
<sm-secrets-list
*ngIf="projectSecrets.secrets?.length > 0 || projectSecrets.project?.write; else contactAdmin"
*ngIf="projectSecrets.secrets?.length > 0 || this.writeAccess(); else contactAdmin"
(deleteSecretsEvent)="openDeleteSecret($event)"
(newSecretEvent)="openNewSecretDialog()"
(editSecretEvent)="openEditSecret($event)"

View File

@@ -1,16 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import {
combineLatest,
combineLatestWith,
filter,
firstValueFrom,
Observable,
startWith,
switchMap,
} from "rxjs";
import { Component, computed, inject, OnInit, Signal } from "@angular/core";
import { ActivatedRoute, ROUTER_OUTLET_DATA } from "@angular/router";
import { combineLatestWith, firstValueFrom, Observable, startWith, switchMap } from "rxjs";
import {
getOrganizationById,
@@ -40,7 +32,6 @@ import {
} from "../../secrets/dialog/secret-view-dialog.component";
import { SecretService } from "../../secrets/secret.service";
import { SecretsListComponent } from "../../shared/secrets-list.component";
import { ProjectService } from "../project.service";
@Component({
selector: "sm-project-secrets",
@@ -54,10 +45,12 @@ export class ProjectSecretsComponent implements OnInit {
private projectId: string;
protected project$: Observable<ProjectView>;
private organizationEnabled: boolean;
protected project = inject(ROUTER_OUTLET_DATA) as Signal<ProjectView>;
readonly writeAccess = computed(() => this.project().write);
readonly projectExists = computed(() => !!this.project());
constructor(
private route: ActivatedRoute,
private projectService: ProjectService,
private secretService: SecretService,
private dialogService: DialogService,
private platformUtilsService: PlatformUtilsService,
@@ -68,21 +61,9 @@ export class ProjectSecretsComponent implements OnInit {
) {}
ngOnInit() {
// Refresh list if project is edited
const currentProjectEdited = this.projectService.project$.pipe(
filter((p) => p?.id === this.projectId),
startWith(null),
);
this.project$ = combineLatest([this.route.params, currentProjectEdited]).pipe(
switchMap(([params, _]) => {
return this.projectService.getByProjectId(params.projectId);
}),
);
this.secrets$ = this.secretService.secret$.pipe(
startWith(null),
combineLatestWith(this.route.params, currentProjectEdited),
combineLatestWith(this.route.params),
switchMap(async ([_, params]) => {
this.organizationId = params.organizationId;
this.projectId = params.projectId;

View File

@@ -36,4 +36,4 @@
{{ "editProject" | i18n }}
</button>
</app-header>
<router-outlet></router-outlet>
<router-outlet [routerOutletData]="this.project$"></router-outlet>

View File

@@ -1,7 +1,6 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { projectAccessGuard } from "./guards/project-access.guard";
import { ProjectPeopleComponent } from "./project/project-people.component";
import { ProjectSecretsComponent } from "./project/project-secrets.component";
import { ProjectServiceAccountsComponent } from "./project/project-service-accounts.component";
@@ -16,7 +15,6 @@ const routes: Routes = [
{
path: ":projectId",
component: ProjectComponent,
canActivate: [projectAccessGuard],
children: [
{
path: "",