mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-28 17:27:50 +01:00
Restructure the org-redirect
guard to be Angular 17+ compliant (#9552)
* Document the `org-redirect` guard in code * Make assertions about the way the `org-redirect` guard should behave * Restructure the `org-redirect` guard to be Angular 17+ compliant * Convert data parameter to function parameter * Convert a data parameter to a function parameter that was missed * Pass redirect function to default organization route
This commit is contained in:
parent
71e8fdb73d
commit
9551691fde
@ -0,0 +1,123 @@
|
|||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { provideRouter } from "@angular/router";
|
||||||
|
import { RouterTestingHarness } from "@angular/router/testing";
|
||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
|
||||||
|
import { organizationRedirectGuard } from "./org-redirect.guard";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: "<h1>This is the home screen!</h1>",
|
||||||
|
})
|
||||||
|
export class HomescreenComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: "<h1>This is the admin console!</h1>",
|
||||||
|
})
|
||||||
|
export class AdminConsoleComponent {}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: "<h1> This is a subroute of the admin console!</h1>",
|
||||||
|
})
|
||||||
|
export class AdminConsoleSubrouteComponent {}
|
||||||
|
|
||||||
|
const orgFactory = (props: Partial<Organization> = {}) =>
|
||||||
|
Object.assign(
|
||||||
|
new Organization(),
|
||||||
|
{
|
||||||
|
id: "myOrgId",
|
||||||
|
enabled: true,
|
||||||
|
type: OrganizationUserType.Admin,
|
||||||
|
},
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("Organization Redirect Guard", () => {
|
||||||
|
let organizationService: MockProxy<OrganizationService>;
|
||||||
|
let routerHarness: RouterTestingHarness;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
organizationService = mock<OrganizationService>();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: OrganizationService, useValue: organizationService },
|
||||||
|
provideRouter([
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: HomescreenComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "organizations/:organizationId",
|
||||||
|
component: AdminConsoleComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "organizations/:organizationId/stringCallback/success",
|
||||||
|
component: AdminConsoleSubrouteComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "organizations/:organizationId/arrayCallback/exponential/success",
|
||||||
|
component: AdminConsoleSubrouteComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "organizations/:organizationId/noCallback",
|
||||||
|
component: AdminConsoleComponent,
|
||||||
|
canActivate: [organizationRedirectGuard()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "organizations/:organizationId/stringCallback",
|
||||||
|
component: AdminConsoleComponent,
|
||||||
|
canActivate: [organizationRedirectGuard(() => "success")],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "organizations/:organizationId/arrayCallback",
|
||||||
|
component: AdminConsoleComponent,
|
||||||
|
canActivate: [organizationRedirectGuard(() => ["exponential", "success"])],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
routerHarness = await RouterTestingHarness.create();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects to `/` if the organization id provided is not found", async () => {
|
||||||
|
const org = orgFactory();
|
||||||
|
organizationService.get.calledWith(org.id).mockResolvedValue(null);
|
||||||
|
await routerHarness.navigateByUrl(`organizations/${org.id}/noCallback`);
|
||||||
|
expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
|
||||||
|
"This is the home screen!",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects to `/organizations/{id}` if no custom redirect is supplied but the user can access the admin onsole", async () => {
|
||||||
|
const org = orgFactory();
|
||||||
|
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||||
|
await routerHarness.navigateByUrl(`organizations/${org.id}/noCallback`);
|
||||||
|
expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
|
||||||
|
"This is the admin console!",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects properly when the redirect callback returns a single string", async () => {
|
||||||
|
const org = orgFactory();
|
||||||
|
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||||
|
await routerHarness.navigateByUrl(`organizations/${org.id}/stringCallback`);
|
||||||
|
expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
|
||||||
|
"This is a subroute of the admin console!",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects properly when the redirect callback returns an array of strings", async () => {
|
||||||
|
const org = orgFactory();
|
||||||
|
organizationService.get.calledWith(org.id).mockResolvedValue(org);
|
||||||
|
await routerHarness.navigateByUrl(`organizations/${org.id}/arrayCallback`);
|
||||||
|
expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe(
|
||||||
|
"This is a subroute of the admin console!",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -1,35 +1,45 @@
|
|||||||
import { Injectable } from "@angular/core";
|
import { inject } from "@angular/core";
|
||||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateFn,
|
||||||
|
Router,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
} from "@angular/router";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
canAccessOrgAdmin,
|
canAccessOrgAdmin,
|
||||||
OrganizationService,
|
OrganizationService,
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
|
||||||
@Injectable({
|
/**
|
||||||
providedIn: "root",
|
*
|
||||||
})
|
* `CanActivateFn` that returns a URL Tree redirecting to a caller provided
|
||||||
export class OrganizationRedirectGuard implements CanActivate {
|
* sub route of `/organizations/{id}/`. If no sub route is provided the URL
|
||||||
constructor(
|
* tree returned will redirect to `/organizations/{id}` if possible, or `/` if
|
||||||
private router: Router,
|
* the user does not have permission to access `organizations/{id}`.
|
||||||
private organizationService: OrganizationService,
|
*/
|
||||||
) {}
|
export function organizationRedirectGuard(
|
||||||
|
customRedirect?: (org: Organization) => string | string[],
|
||||||
|
): CanActivateFn {
|
||||||
|
return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||||
|
const router = inject(Router);
|
||||||
|
const organizationService = inject(OrganizationService);
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
const org = await organizationService.get(route.params.organizationId);
|
||||||
const org = await this.organizationService.get(route.params.organizationId);
|
|
||||||
|
|
||||||
const customRedirect = route.data?.autoRedirectCallback;
|
if (customRedirect != null) {
|
||||||
if (customRedirect) {
|
|
||||||
let redirectPath = customRedirect(org);
|
let redirectPath = customRedirect(org);
|
||||||
if (typeof redirectPath === "string") {
|
if (typeof redirectPath === "string") {
|
||||||
redirectPath = [redirectPath];
|
redirectPath = [redirectPath];
|
||||||
}
|
}
|
||||||
return this.router.createUrlTree([state.url, ...redirectPath]);
|
return router.createUrlTree([state.url, ...redirectPath]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canAccessOrgAdmin(org)) {
|
if (org != null && canAccessOrgAdmin(org)) {
|
||||||
return this.router.createUrlTree(["/organizations", org.id]);
|
return router.createUrlTree(["/organizations", org.id]);
|
||||||
}
|
|
||||||
return this.router.createUrlTree(["/"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return router.createUrlTree(["/"]);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
|
||||||
import { OrganizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard";
|
import { OrganizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard";
|
||||||
import { OrganizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard";
|
import { organizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard";
|
||||||
import { OrganizationLayoutComponent } from "../../admin-console/organizations/layouts/organization-layout.component";
|
import { OrganizationLayoutComponent } from "../../admin-console/organizations/layouts/organization-layout.component";
|
||||||
import { GroupsComponent } from "../../admin-console/organizations/manage/groups.component";
|
import { GroupsComponent } from "../../admin-console/organizations/manage/groups.component";
|
||||||
import { deepLinkGuard } from "../../auth/guards/deep-link.guard";
|
import { deepLinkGuard } from "../../auth/guards/deep-link.guard";
|
||||||
@ -31,10 +31,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
pathMatch: "full",
|
pathMatch: "full",
|
||||||
canActivate: [OrganizationRedirectGuard],
|
canActivate: [organizationRedirectGuard(getOrganizationRoute)],
|
||||||
data: {
|
|
||||||
autoRedirectCallback: getOrganizationRoute,
|
|
||||||
},
|
|
||||||
children: [], // This is required to make the auto redirect work, },
|
children: [], // This is required to make the auto redirect work, },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,7 @@ import { UnsecuredWebsitesReportComponent } from "../../../admin-console/organiz
|
|||||||
import { WeakPasswordsReportComponent } from "../../../admin-console/organizations/tools/weak-passwords-report.component";
|
import { WeakPasswordsReportComponent } from "../../../admin-console/organizations/tools/weak-passwords-report.component";
|
||||||
import { IsPaidOrgGuard } from "../guards/is-paid-org.guard";
|
import { IsPaidOrgGuard } from "../guards/is-paid-org.guard";
|
||||||
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||||
import { OrganizationRedirectGuard } from "../guards/org-redirect.guard";
|
import { organizationRedirectGuard } from "../guards/org-redirect.guard";
|
||||||
import { EventsComponent } from "../manage/events.component";
|
import { EventsComponent } from "../manage/events.component";
|
||||||
|
|
||||||
import { ReportsHomeComponent } from "./reports-home.component";
|
import { ReportsHomeComponent } from "./reports-home.component";
|
||||||
@ -25,10 +25,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
pathMatch: "full",
|
pathMatch: "full",
|
||||||
canActivate: [OrganizationRedirectGuard],
|
canActivate: [organizationRedirectGuard(getReportRoute)],
|
||||||
data: {
|
|
||||||
autoRedirectCallback: getReportRoute,
|
|
||||||
},
|
|
||||||
children: [], // This is required to make the auto redirect work,
|
children: [], // This is required to make the auto redirect work,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,7 @@ import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractio
|
|||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
|
||||||
import { OrganizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
|
import { OrganizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
|
||||||
import { OrganizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
|
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
|
||||||
import { PoliciesComponent } from "../../organizations/policies";
|
import { PoliciesComponent } from "../../organizations/policies";
|
||||||
|
|
||||||
import { AccountComponent } from "./account.component";
|
import { AccountComponent } from "./account.component";
|
||||||
@ -20,10 +20,7 @@ const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
pathMatch: "full",
|
pathMatch: "full",
|
||||||
canActivate: [OrganizationRedirectGuard],
|
canActivate: [organizationRedirectGuard(getSettingsRoute)],
|
||||||
data: {
|
|
||||||
autoRedirectCallback: getSettingsRoute,
|
|
||||||
},
|
|
||||||
children: [], // This is required to make the auto redirect work,
|
children: [], // This is required to make the auto redirect work,
|
||||||
},
|
},
|
||||||
{ path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } },
|
{ path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } },
|
||||||
|
Loading…
Reference in New Issue
Block a user