Browse Source
* Replace Permissions enum and helper methods with callbacks * Remove scim feature flag * Check if org has feature enabled as part of canManage checks * Pin jest-mock-extended at v2.0.6 to fix compilation errorpull/3311/head
32 changed files with 474 additions and 282 deletions
@ -0,0 +1,163 @@
@@ -0,0 +1,163 @@
|
||||
import { |
||||
ActivatedRouteSnapshot, |
||||
convertToParamMap, |
||||
Router, |
||||
RouterStateSnapshot, |
||||
} from "@angular/router"; |
||||
import { mock, MockProxy } from "jest-mock-extended"; |
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; |
||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; |
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; |
||||
import { SyncService } from "@bitwarden/common/abstractions/sync.service"; |
||||
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; |
||||
import { Organization } from "@bitwarden/common/models/domain/organization"; |
||||
|
||||
import { OrganizationPermissionsGuard } from "./org-permissions.guard"; |
||||
|
||||
const orgFactory = (props: Partial<Organization> = {}) => |
||||
Object.assign( |
||||
new Organization(), |
||||
{ |
||||
id: "myOrgId", |
||||
enabled: true, |
||||
type: OrganizationUserType.Admin, |
||||
}, |
||||
props |
||||
); |
||||
|
||||
describe("Organization Permissions Guard", () => { |
||||
let router: MockProxy<Router>; |
||||
let organizationService: MockProxy<OrganizationService>; |
||||
let state: MockProxy<RouterStateSnapshot>; |
||||
let route: MockProxy<ActivatedRouteSnapshot>; |
||||
|
||||
let organizationPermissionsGuard: OrganizationPermissionsGuard; |
||||
|
||||
beforeEach(() => { |
||||
router = mock<Router>(); |
||||
organizationService = mock<OrganizationService>(); |
||||
state = mock<RouterStateSnapshot>(); |
||||
route = mock<ActivatedRouteSnapshot>({ |
||||
params: { |
||||
organizationId: orgFactory().id, |
||||
}, |
||||
data: { |
||||
organizationPermissions: null, |
||||
}, |
||||
}); |
||||
|
||||
organizationPermissionsGuard = new OrganizationPermissionsGuard( |
||||
router, |
||||
organizationService, |
||||
mock<PlatformUtilsService>(), |
||||
mock<I18nService>(), |
||||
mock<SyncService>() |
||||
); |
||||
}); |
||||
|
||||
it("blocks navigation if organization does not exist", async () => { |
||||
organizationService.get.mockResolvedValue(null); |
||||
|
||||
const actual = await organizationPermissionsGuard.canActivate(route, state); |
||||
|
||||
expect(actual).not.toBe(true); |
||||
}); |
||||
|
||||
it("permits navigation if no permissions are specified", async () => { |
||||
const org = orgFactory(); |
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org); |
||||
|
||||
const actual = await organizationPermissionsGuard.canActivate(route, state); |
||||
|
||||
expect(actual).toBe(true); |
||||
}); |
||||
|
||||
it("permits navigation if the user has permissions", async () => { |
||||
const permissionsCallback = jest.fn(); |
||||
permissionsCallback.mockImplementation((org) => true); |
||||
route.data = { |
||||
organizationPermissions: permissionsCallback, |
||||
}; |
||||
|
||||
const org = orgFactory(); |
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org); |
||||
|
||||
const actual = await organizationPermissionsGuard.canActivate(route, state); |
||||
|
||||
expect(permissionsCallback).toHaveBeenCalled(); |
||||
expect(actual).toBe(true); |
||||
}); |
||||
|
||||
describe("if the user does not have permissions", () => { |
||||
it("and there is no Item ID, block navigation", async () => { |
||||
const permissionsCallback = jest.fn(); |
||||
permissionsCallback.mockImplementation((org) => false); |
||||
route.data = { |
||||
organizationPermissions: permissionsCallback, |
||||
}; |
||||
|
||||
state = mock<RouterStateSnapshot>({ |
||||
root: mock<ActivatedRouteSnapshot>({ |
||||
queryParamMap: convertToParamMap({}), |
||||
}), |
||||
}); |
||||
|
||||
const org = orgFactory(); |
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org); |
||||
|
||||
const actual = await organizationPermissionsGuard.canActivate(route, state); |
||||
|
||||
expect(permissionsCallback).toHaveBeenCalled(); |
||||
expect(actual).not.toBe(true); |
||||
}); |
||||
|
||||
it("and there is an Item ID, redirect to the item in the individual vault", async () => { |
||||
route.data = { |
||||
organizationPermissions: (org: Organization) => false, |
||||
}; |
||||
state = mock<RouterStateSnapshot>({ |
||||
root: mock<ActivatedRouteSnapshot>({ |
||||
queryParamMap: convertToParamMap({ |
||||
itemId: "myItemId", |
||||
}), |
||||
}), |
||||
}); |
||||
const org = orgFactory(); |
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org); |
||||
|
||||
const actual = await organizationPermissionsGuard.canActivate(route, state); |
||||
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(["/vault"], { |
||||
queryParams: { itemId: "myItemId" }, |
||||
}); |
||||
expect(actual).not.toBe(true); |
||||
}); |
||||
}); |
||||
|
||||
describe("given a disabled organization", () => { |
||||
it("blocks navigation if user is not an owner", async () => { |
||||
const org = orgFactory({ |
||||
type: OrganizationUserType.Admin, |
||||
enabled: false, |
||||
}); |
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org); |
||||
|
||||
const actual = await organizationPermissionsGuard.canActivate(route, state); |
||||
|
||||
expect(actual).not.toBe(true); |
||||
}); |
||||
|
||||
it("permits navigation if user is an owner", async () => { |
||||
const org = orgFactory({ |
||||
type: OrganizationUserType.Owner, |
||||
enabled: false, |
||||
}); |
||||
organizationService.get.calledWith(org.id).mockResolvedValue(org); |
||||
|
||||
const actual = await organizationPermissionsGuard.canActivate(route, state); |
||||
|
||||
expect(actual).toBe(true); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
import { Organization } from "@bitwarden/common/models/domain/organization"; |
||||
|
||||
export function canAccessToolsTab(org: Organization): boolean { |
||||
return org.canAccessImportExport || org.canAccessReports; |
||||
} |
||||
|
||||
export function canAccessSettingsTab(org: Organization): boolean { |
||||
return org.isOwner; |
||||
} |
||||
|
||||
export function canAccessManageTab(org: Organization): boolean { |
||||
return ( |
||||
org.canCreateNewCollections || |
||||
org.canEditAnyCollection || |
||||
org.canDeleteAnyCollection || |
||||
org.canEditAssignedCollections || |
||||
org.canDeleteAssignedCollections || |
||||
org.canAccessEventLogs || |
||||
org.canManageGroups || |
||||
org.canManageUsers || |
||||
org.canManagePolicies || |
||||
org.canManageSso || |
||||
org.canManageScim |
||||
); |
||||
} |
||||
|
||||
export function canAccessOrgAdmin(org: Organization): boolean { |
||||
return canAccessToolsTab(org) || canAccessSettingsTab(org) || canAccessManageTab(org); |
||||
} |
||||
@ -1,50 +0,0 @@
@@ -1,50 +0,0 @@
|
||||
import { Permissions } from "@bitwarden/common/enums/permissions"; |
||||
import { Organization } from "@bitwarden/common/models/domain/organization"; |
||||
|
||||
const permissions = { |
||||
manage: [ |
||||
Permissions.CreateNewCollections, |
||||
Permissions.EditAnyCollection, |
||||
Permissions.DeleteAnyCollection, |
||||
Permissions.EditAssignedCollections, |
||||
Permissions.DeleteAssignedCollections, |
||||
Permissions.AccessEventLogs, |
||||
Permissions.ManageGroups, |
||||
Permissions.ManageUsers, |
||||
Permissions.ManagePolicies, |
||||
Permissions.ManageSso, |
||||
Permissions.ManageScim, |
||||
], |
||||
tools: [Permissions.AccessImportExport, Permissions.AccessReports], |
||||
settings: [Permissions.ManageOrganization], |
||||
}; |
||||
|
||||
export class NavigationPermissionsService { |
||||
static getPermissions(route: keyof typeof permissions | "admin") { |
||||
if (route === "admin") { |
||||
return Object.values(permissions).reduce((previous, current) => previous.concat(current), []); |
||||
} |
||||
|
||||
return permissions[route]; |
||||
} |
||||
|
||||
static canAccessAdmin(organization: Organization): boolean { |
||||
return ( |
||||
this.canAccessTools(organization) || |
||||
this.canAccessSettings(organization) || |
||||
this.canAccessManage(organization) |
||||
); |
||||
} |
||||
|
||||
static canAccessTools(organization: Organization): boolean { |
||||
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("tools")); |
||||
} |
||||
|
||||
static canAccessSettings(organization: Organization): boolean { |
||||
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("settings")); |
||||
} |
||||
|
||||
static canAccessManage(organization: Organization): boolean { |
||||
return organization.hasAnyPermission(NavigationPermissionsService.getPermissions("manage")); |
||||
} |
||||
} |
||||
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
const { pathsToModuleNameMapper } = require("ts-jest"); |
||||
|
||||
const { compilerOptions } = require("./tsconfig"); |
||||
|
||||
const sharedConfig = require("../../libs/shared/jest.config.base"); |
||||
|
||||
module.exports = { |
||||
...sharedConfig, |
||||
preset: "jest-preset-angular", |
||||
setupFilesAfterEnv: ["../../apps/web/test.setup.ts"], |
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { |
||||
prefix: "<rootDir>/", |
||||
}), |
||||
modulePathIgnorePatterns: ["jslib"], |
||||
}; |
||||
@ -0,0 +1,124 @@
@@ -0,0 +1,124 @@
|
||||
import { ActivatedRouteSnapshot, Router } from "@angular/router"; |
||||
import { mock, MockProxy } from "jest-mock-extended"; |
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; |
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; |
||||
import { ProviderService } from "@bitwarden/common/abstractions/provider.service"; |
||||
import { ProviderUserType } from "@bitwarden/common/enums/providerUserType"; |
||||
import { Provider } from "@bitwarden/common/models/domain/provider"; |
||||
|
||||
import { ProviderPermissionsGuard } from "./provider-permissions.guard"; |
||||
|
||||
const providerFactory = (props: Partial<Provider> = {}) => |
||||
Object.assign( |
||||
new Provider(), |
||||
{ |
||||
id: "myProviderId", |
||||
enabled: true, |
||||
type: ProviderUserType.ServiceUser, |
||||
}, |
||||
props |
||||
); |
||||
|
||||
describe("Provider Permissions Guard", () => { |
||||
let router: MockProxy<Router>; |
||||
let providerService: MockProxy<ProviderService>; |
||||
let route: MockProxy<ActivatedRouteSnapshot>; |
||||
|
||||
let providerPermissionsGuard: ProviderPermissionsGuard; |
||||
|
||||
beforeEach(() => { |
||||
router = mock<Router>(); |
||||
providerService = mock<ProviderService>(); |
||||
route = mock<ActivatedRouteSnapshot>({ |
||||
params: { |
||||
providerId: providerFactory().id, |
||||
}, |
||||
data: { |
||||
providerPermissions: null, |
||||
}, |
||||
}); |
||||
|
||||
providerPermissionsGuard = new ProviderPermissionsGuard( |
||||
providerService, |
||||
router, |
||||
mock<PlatformUtilsService>(), |
||||
mock<I18nService>() |
||||
); |
||||
}); |
||||
|
||||
it("blocks navigation if provider does not exist", async () => { |
||||
providerService.get.mockResolvedValue(null); |
||||
|
||||
const actual = await providerPermissionsGuard.canActivate(route); |
||||
|
||||
expect(actual).not.toBe(true); |
||||
}); |
||||
|
||||
it("permits navigation if no permissions are specified", async () => { |
||||
const provider = providerFactory(); |
||||
providerService.get.calledWith(provider.id).mockResolvedValue(provider); |
||||
|
||||
const actual = await providerPermissionsGuard.canActivate(route); |
||||
|
||||
expect(actual).toBe(true); |
||||
}); |
||||
|
||||
it("permits navigation if the user has permissions", async () => { |
||||
const permissionsCallback = jest.fn(); |
||||
permissionsCallback.mockImplementation((provider) => true); |
||||
route.data = { |
||||
providerPermissions: permissionsCallback, |
||||
}; |
||||
|
||||
const provider = providerFactory(); |
||||
providerService.get.calledWith(provider.id).mockResolvedValue(provider); |
||||
|
||||
const actual = await providerPermissionsGuard.canActivate(route); |
||||
|
||||
expect(permissionsCallback).toHaveBeenCalled(); |
||||
expect(actual).toBe(true); |
||||
}); |
||||
|
||||
it("blocks navigation if the user does not have permissions", async () => { |
||||
const permissionsCallback = jest.fn(); |
||||
permissionsCallback.mockImplementation((org) => false); |
||||
route.data = { |
||||
providerPermissions: permissionsCallback, |
||||
}; |
||||
|
||||
const provider = providerFactory(); |
||||
providerService.get.calledWith(provider.id).mockResolvedValue(provider); |
||||
|
||||
const actual = await providerPermissionsGuard.canActivate(route); |
||||
|
||||
expect(permissionsCallback).toHaveBeenCalled(); |
||||
expect(actual).not.toBe(true); |
||||
}); |
||||
|
||||
describe("given a disabled organization", () => { |
||||
it("blocks navigation if user is not an admin", async () => { |
||||
const org = providerFactory({ |
||||
type: ProviderUserType.ServiceUser, |
||||
enabled: false, |
||||
}); |
||||
providerService.get.calledWith(org.id).mockResolvedValue(org); |
||||
|
||||
const actual = await providerPermissionsGuard.canActivate(route); |
||||
|
||||
expect(actual).not.toBe(true); |
||||
}); |
||||
|
||||
it("permits navigation if user is an admin", async () => { |
||||
const org = providerFactory({ |
||||
type: ProviderUserType.ProviderAdmin, |
||||
enabled: false, |
||||
}); |
||||
providerService.get.calledWith(org.id).mockResolvedValue(org); |
||||
|
||||
const actual = await providerPermissionsGuard.canActivate(route); |
||||
|
||||
expect(actual).toBe(true); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,26 +0,0 @@
@@ -1,26 +0,0 @@
|
||||
import { Injectable } from "@angular/core"; |
||||
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router"; |
||||
|
||||
import { ProviderService } from "@bitwarden/common/abstractions/provider.service"; |
||||
import { Permissions } from "@bitwarden/common/enums/permissions"; |
||||
|
||||
@Injectable() |
||||
export class PermissionsGuard implements CanActivate { |
||||
constructor(private providerService: ProviderService, private router: Router) {} |
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot) { |
||||
const provider = await this.providerService.get(route.params.providerId); |
||||
const permissions = route.data == null ? null : (route.data.permissions as Permissions[]); |
||||
|
||||
if ( |
||||
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) || |
||||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) || |
||||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers) |
||||
) { |
||||
return true; |
||||
} |
||||
|
||||
this.router.navigate(["/providers", provider.id]); |
||||
return false; |
||||
} |
||||
} |
||||
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
{ |
||||
"extends": "./tsconfig.json", |
||||
"files": ["../../apps/web/test.setup.ts"] |
||||
} |
||||
@ -1,29 +0,0 @@
@@ -1,29 +0,0 @@
|
||||
export enum Permissions { |
||||
AccessEventLogs, |
||||
AccessImportExport, |
||||
AccessReports, |
||||
/** |
||||
* @deprecated Sep 29 2021: This permission has been split out to `createNewCollections`, `editAnyCollection`, and |
||||
* `deleteAnyCollection`. It exists here for backwards compatibility with Server versions <= 1.43.0 |
||||
*/ |
||||
ManageAllCollections, |
||||
/** |
||||
* @deprecated Sep 29 2021: This permission has been split out to `editAssignedCollections` and |
||||
* `deleteAssignedCollections`. It exists here for backwards compatibility with Server versions <= 1.43.0 |
||||
*/ |
||||
ManageAssignedCollections, |
||||
ManageGroups, |
||||
ManageOrganization, |
||||
ManagePolicies, |
||||
ManageProvider, |
||||
ManageUsers, |
||||
ManageUsersPassword, |
||||
CreateNewCollections, |
||||
EditAnyCollection, |
||||
DeleteAnyCollection, |
||||
EditAssignedCollections, |
||||
DeleteAssignedCollections, |
||||
ManageSso, |
||||
ManageBilling, |
||||
ManageScim, |
||||
} |
||||
Loading…
Reference in new issue