Restructure the `provider-permissions` guard to be Angular 17+ compliant (#9609)
* Document the `provider-permissions` guard in code * Restructure the `provider-permissions` guard to be Angular 17+ compliant
This commit is contained in:
parent
b7e3f5bc68
commit
72355d26e1
|
@ -1,4 +1,5 @@
|
||||||
import { ActivatedRouteSnapshot, Router } from "@angular/router";
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
|
@ -7,7 +8,7 @@ import { Provider } from "@bitwarden/common/admin-console/models/domain/provider
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
import { ProviderPermissionsGuard } from "./provider-permissions.guard";
|
import { providerPermissionsGuard } from "./provider-permissions.guard";
|
||||||
|
|
||||||
const providerFactory = (props: Partial<Provider> = {}) =>
|
const providerFactory = (props: Partial<Provider> = {}) =>
|
||||||
Object.assign(
|
Object.assign(
|
||||||
|
@ -21,14 +22,11 @@ const providerFactory = (props: Partial<Provider> = {}) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe("Provider Permissions Guard", () => {
|
describe("Provider Permissions Guard", () => {
|
||||||
let router: MockProxy<Router>;
|
|
||||||
let providerService: MockProxy<ProviderService>;
|
let providerService: MockProxy<ProviderService>;
|
||||||
let route: MockProxy<ActivatedRouteSnapshot>;
|
let route: MockProxy<ActivatedRouteSnapshot>;
|
||||||
|
let state: MockProxy<RouterStateSnapshot>;
|
||||||
let providerPermissionsGuard: ProviderPermissionsGuard;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
router = mock<Router>();
|
|
||||||
providerService = mock<ProviderService>();
|
providerService = mock<ProviderService>();
|
||||||
route = mock<ActivatedRouteSnapshot>({
|
route = mock<ActivatedRouteSnapshot>({
|
||||||
params: {
|
params: {
|
||||||
|
@ -38,19 +36,22 @@ describe("Provider Permissions Guard", () => {
|
||||||
providerPermissions: null,
|
providerPermissions: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
TestBed.configureTestingModule({
|
||||||
providerPermissionsGuard = new ProviderPermissionsGuard(
|
providers: [
|
||||||
providerService,
|
{ provide: ProviderService, useValue: providerService },
|
||||||
router,
|
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||||
mock<PlatformUtilsService>(),
|
{ provide: I18nService, useValue: mock<I18nService>() },
|
||||||
mock<I18nService>(),
|
{ provide: Router, useValue: mock<Router>() },
|
||||||
);
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blocks navigation if provider does not exist", async () => {
|
it("blocks navigation if provider does not exist", async () => {
|
||||||
providerService.get.mockResolvedValue(null);
|
providerService.get.mockResolvedValue(null);
|
||||||
|
|
||||||
const actual = await providerPermissionsGuard.canActivate(route);
|
const actual = await TestBed.runInInjectionContext(
|
||||||
|
async () => await providerPermissionsGuard()(route, state),
|
||||||
|
);
|
||||||
|
|
||||||
expect(actual).not.toBe(true);
|
expect(actual).not.toBe(true);
|
||||||
});
|
});
|
||||||
|
@ -59,22 +60,23 @@ describe("Provider Permissions Guard", () => {
|
||||||
const provider = providerFactory();
|
const provider = providerFactory();
|
||||||
providerService.get.calledWith(provider.id).mockResolvedValue(provider);
|
providerService.get.calledWith(provider.id).mockResolvedValue(provider);
|
||||||
|
|
||||||
const actual = await providerPermissionsGuard.canActivate(route);
|
const actual = await TestBed.runInInjectionContext(
|
||||||
|
async () => await providerPermissionsGuard()(route, state),
|
||||||
|
);
|
||||||
|
|
||||||
expect(actual).toBe(true);
|
expect(actual).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("permits navigation if the user has permissions", async () => {
|
it("permits navigation if the user has permissions", async () => {
|
||||||
const permissionsCallback = jest.fn();
|
const permissionsCallback = jest.fn();
|
||||||
permissionsCallback.mockImplementation((provider) => true);
|
permissionsCallback.mockImplementation((_provider) => true);
|
||||||
route.data = {
|
|
||||||
providerPermissions: permissionsCallback,
|
|
||||||
};
|
|
||||||
|
|
||||||
const provider = providerFactory();
|
const provider = providerFactory();
|
||||||
providerService.get.calledWith(provider.id).mockResolvedValue(provider);
|
providerService.get.calledWith(provider.id).mockResolvedValue(provider);
|
||||||
|
|
||||||
const actual = await providerPermissionsGuard.canActivate(route);
|
const actual = await TestBed.runInInjectionContext(
|
||||||
|
async () => await providerPermissionsGuard(permissionsCallback)(route, state),
|
||||||
|
);
|
||||||
|
|
||||||
expect(permissionsCallback).toHaveBeenCalled();
|
expect(permissionsCallback).toHaveBeenCalled();
|
||||||
expect(actual).toBe(true);
|
expect(actual).toBe(true);
|
||||||
|
@ -82,15 +84,13 @@ describe("Provider Permissions Guard", () => {
|
||||||
|
|
||||||
it("blocks navigation if the user does not have permissions", async () => {
|
it("blocks navigation if the user does not have permissions", async () => {
|
||||||
const permissionsCallback = jest.fn();
|
const permissionsCallback = jest.fn();
|
||||||
permissionsCallback.mockImplementation((org) => false);
|
permissionsCallback.mockImplementation((_org) => false);
|
||||||
route.data = {
|
|
||||||
providerPermissions: permissionsCallback,
|
|
||||||
};
|
|
||||||
|
|
||||||
const provider = providerFactory();
|
const provider = providerFactory();
|
||||||
providerService.get.calledWith(provider.id).mockResolvedValue(provider);
|
providerService.get.calledWith(provider.id).mockResolvedValue(provider);
|
||||||
|
|
||||||
const actual = await providerPermissionsGuard.canActivate(route);
|
const actual = await TestBed.runInInjectionContext(
|
||||||
|
async () => await providerPermissionsGuard(permissionsCallback)(route, state),
|
||||||
|
);
|
||||||
|
|
||||||
expect(permissionsCallback).toHaveBeenCalled();
|
expect(permissionsCallback).toHaveBeenCalled();
|
||||||
expect(actual).not.toBe(true);
|
expect(actual).not.toBe(true);
|
||||||
|
@ -104,7 +104,9 @@ describe("Provider Permissions Guard", () => {
|
||||||
});
|
});
|
||||||
providerService.get.calledWith(org.id).mockResolvedValue(org);
|
providerService.get.calledWith(org.id).mockResolvedValue(org);
|
||||||
|
|
||||||
const actual = await providerPermissionsGuard.canActivate(route);
|
const actual = await TestBed.runInInjectionContext(
|
||||||
|
async () => await providerPermissionsGuard()(route, state),
|
||||||
|
);
|
||||||
|
|
||||||
expect(actual).not.toBe(true);
|
expect(actual).not.toBe(true);
|
||||||
});
|
});
|
||||||
|
@ -116,7 +118,9 @@ describe("Provider Permissions Guard", () => {
|
||||||
});
|
});
|
||||||
providerService.get.calledWith(org.id).mockResolvedValue(org);
|
providerService.get.calledWith(org.id).mockResolvedValue(org);
|
||||||
|
|
||||||
const actual = await providerPermissionsGuard.canActivate(route);
|
const actual = await TestBed.runInInjectionContext(
|
||||||
|
async () => await providerPermissionsGuard()(route, state),
|
||||||
|
);
|
||||||
|
|
||||||
expect(actual).toBe(true);
|
expect(actual).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,39 +1,61 @@
|
||||||
import { Injectable } from "@angular/core";
|
import { inject } from "@angular/core";
|
||||||
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivateFn,
|
||||||
|
Router,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
} from "@angular/router";
|
||||||
|
|
||||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
@Injectable()
|
/**
|
||||||
export class ProviderPermissionsGuard implements CanActivate {
|
* `CanActivateFn` that asserts the logged in user has permission to access
|
||||||
constructor(
|
* the page being navigated to. Two high-level checks are performed:
|
||||||
private providerService: ProviderService,
|
*
|
||||||
private router: Router,
|
* 1. If the user is not a member of the provider in the URL parameters, they
|
||||||
private platformUtilsService: PlatformUtilsService,
|
* are redirected to the home screen.
|
||||||
private i18nService: I18nService,
|
* 2. If the provider in the URL parameters is disabled and the user is not
|
||||||
) {}
|
* an admin, they are redirected to the home screen.
|
||||||
|
*
|
||||||
|
* In addition to these high level checks the guard accepts a callback
|
||||||
|
* function as an argument that will be called to check for more granular
|
||||||
|
* permissions. Based on the return from this callback one of the following
|
||||||
|
* will happen:
|
||||||
|
*
|
||||||
|
* 1. If the logged in user does not have the required permissions they are
|
||||||
|
* redirected to `/providers`.
|
||||||
|
* 2. If the logged in user does have the required permissions navigation
|
||||||
|
* proceeds as expected.
|
||||||
|
*/
|
||||||
|
export function providerPermissionsGuard(
|
||||||
|
permissionsCallback?: (provider: Provider) => boolean,
|
||||||
|
): CanActivateFn {
|
||||||
|
return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||||
|
const providerService = inject(ProviderService);
|
||||||
|
const router = inject(Router);
|
||||||
|
const platformUtilsService = inject(PlatformUtilsService);
|
||||||
|
const i18nService = inject(I18nService);
|
||||||
|
|
||||||
async canActivate(route: ActivatedRouteSnapshot) {
|
const provider = await providerService.get(route.params.providerId);
|
||||||
const provider = await this.providerService.get(route.params.providerId);
|
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
return this.router.createUrlTree(["/"]);
|
return router.createUrlTree(["/"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!provider.isProviderAdmin && !provider.enabled) {
|
if (!provider.isProviderAdmin && !provider.enabled) {
|
||||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("providerIsDisabled"));
|
platformUtilsService.showToast("error", null, i18nService.t("providerIsDisabled"));
|
||||||
return this.router.createUrlTree(["/"]);
|
return router.createUrlTree(["/"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissionsCallback: (provider: Provider) => boolean = route.data?.providerPermissions;
|
|
||||||
const hasSpecifiedPermissions = permissionsCallback == null || permissionsCallback(provider);
|
const hasSpecifiedPermissions = permissionsCallback == null || permissionsCallback(provider);
|
||||||
|
|
||||||
if (!hasSpecifiedPermissions) {
|
if (!hasSpecifiedPermissions) {
|
||||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied"));
|
platformUtilsService.showToast("error", null, i18nService.t("accessDenied"));
|
||||||
return this.router.createUrlTree(["/providers", provider.id]);
|
return router.createUrlTree(["/providers", provider.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
|
|
||||||
import { ClientsComponent } from "./clients/clients.component";
|
import { ClientsComponent } from "./clients/clients.component";
|
||||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||||
import { ProviderPermissionsGuard } from "./guards/provider-permissions.guard";
|
import { providerPermissionsGuard } from "./guards/provider-permissions.guard";
|
||||||
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
||||||
import { EventsComponent } from "./manage/events.component";
|
import { EventsComponent } from "./manage/events.component";
|
||||||
import { PeopleComponent } from "./manage/people.component";
|
import { PeopleComponent } from "./manage/people.component";
|
||||||
|
@ -76,7 +76,7 @@ const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: ":providerId",
|
path: ":providerId",
|
||||||
component: ProvidersLayoutComponent,
|
component: ProvidersLayoutComponent,
|
||||||
canActivate: [ProviderPermissionsGuard],
|
canActivate: [providerPermissionsGuard()],
|
||||||
children: [
|
children: [
|
||||||
{ path: "", pathMatch: "full", redirectTo: "clients" },
|
{ path: "", pathMatch: "full", redirectTo: "clients" },
|
||||||
{ path: "clients/create", component: CreateOrganizationComponent },
|
{ path: "clients/create", component: CreateOrganizationComponent },
|
||||||
|
@ -98,27 +98,28 @@ const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "people",
|
path: "people",
|
||||||
component: PeopleComponent,
|
component: PeopleComponent,
|
||||||
canActivate: [ProviderPermissionsGuard],
|
canActivate: [
|
||||||
|
providerPermissionsGuard((provider: Provider) => provider.canManageUsers),
|
||||||
|
],
|
||||||
data: {
|
data: {
|
||||||
titleId: "people",
|
titleId: "people",
|
||||||
providerPermissions: (provider: Provider) => provider.canManageUsers,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "events",
|
path: "events",
|
||||||
component: EventsComponent,
|
component: EventsComponent,
|
||||||
canActivate: [ProviderPermissionsGuard],
|
canActivate: [
|
||||||
|
providerPermissionsGuard((provider: Provider) => provider.canAccessEventLogs),
|
||||||
|
],
|
||||||
data: {
|
data: {
|
||||||
titleId: "eventLogs",
|
titleId: "eventLogs",
|
||||||
providerPermissions: (provider: Provider) => provider.canAccessEventLogs,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "billing",
|
path: "billing",
|
||||||
canActivate: [ProviderPermissionsGuard, hasConsolidatedBilling],
|
canActivate: [hasConsolidatedBilling],
|
||||||
data: { providerPermissions: (provider: Provider) => provider.isProviderAdmin },
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
|
@ -128,7 +129,7 @@ const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "subscription",
|
path: "subscription",
|
||||||
component: ProviderSubscriptionComponent,
|
component: ProviderSubscriptionComponent,
|
||||||
canActivate: [ProviderPermissionsGuard],
|
canActivate: [providerPermissionsGuard()],
|
||||||
data: {
|
data: {
|
||||||
titleId: "subscription",
|
titleId: "subscription",
|
||||||
},
|
},
|
||||||
|
@ -136,7 +137,7 @@ const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "history",
|
path: "history",
|
||||||
component: ProviderBillingHistoryComponent,
|
component: ProviderBillingHistoryComponent,
|
||||||
canActivate: [ProviderPermissionsGuard],
|
canActivate: [providerPermissionsGuard()],
|
||||||
data: {
|
data: {
|
||||||
titleId: "billingHistory",
|
titleId: "billingHistory",
|
||||||
},
|
},
|
||||||
|
@ -154,10 +155,11 @@ const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: "account",
|
path: "account",
|
||||||
component: AccountComponent,
|
component: AccountComponent,
|
||||||
canActivate: [ProviderPermissionsGuard],
|
canActivate: [
|
||||||
|
providerPermissionsGuard((provider: Provider) => provider.isProviderAdmin),
|
||||||
|
],
|
||||||
data: {
|
data: {
|
||||||
titleId: "myProvider",
|
titleId: "myProvider",
|
||||||
providerPermissions: (provider: Provider) => provider.isProviderAdmin,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -25,7 +25,6 @@ import { ProviderSubscriptionStatusComponent } from "../../billing/providers/sub
|
||||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||||
import { ClientsComponent } from "./clients/clients.component";
|
import { ClientsComponent } from "./clients/clients.component";
|
||||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||||
import { ProviderPermissionsGuard } from "./guards/provider-permissions.guard";
|
|
||||||
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
import { AcceptProviderComponent } from "./manage/accept-provider.component";
|
||||||
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
|
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
|
||||||
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
|
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component";
|
||||||
|
@ -77,6 +76,6 @@ import { SetupComponent } from "./setup/setup.component";
|
||||||
ProviderPaymentMethodComponent,
|
ProviderPaymentMethodComponent,
|
||||||
ProviderSubscriptionStatusComponent,
|
ProviderSubscriptionStatusComponent,
|
||||||
],
|
],
|
||||||
providers: [WebProviderService, ProviderPermissionsGuard],
|
providers: [WebProviderService],
|
||||||
})
|
})
|
||||||
export class ProvidersModule {}
|
export class ProvidersModule {}
|
||||||
|
|
Loading…
Reference in New Issue