1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-20 01:13:48 +00:00

[EC-416] Refactor organization permission checks (#3252)

* 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 error
This commit is contained in:
Thomas Rittson
2022-08-16 00:08:06 +10:00
committed by GitHub
parent 96d5f50c7f
commit d30701ada7
32 changed files with 474 additions and 282 deletions

View File

@@ -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);
});
});
});

View File

@@ -5,12 +5,14 @@ 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 { Permissions } from "@bitwarden/common/enums/permissions";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { canAccessOrgAdmin } from "../navigation-permissions";
@Injectable({
providedIn: "root",
})
export class PermissionsGuard implements CanActivate {
export class OrganizationPermissionsGuard implements CanActivate {
constructor(
private router: Router,
private organizationService: OrganizationService,
@@ -39,8 +41,11 @@ export class PermissionsGuard implements CanActivate {
return this.router.createUrlTree(["/"]);
}
const permissions = route.data == null ? [] : (route.data.permissions as Permissions[]);
if (permissions != null && !org.hasAnyPermission(permissions)) {
const permissionsCallback: (organization: Organization) => boolean =
route.data?.organizationPermissions;
const hasPermissions = permissionsCallback == null || permissionsCallback(org);
if (!hasPermissions) {
// Handle linkable ciphers for organizations the user only has view access to
// https://bitwarden.atlassian.net/browse/EC-203
const cipherId =
@@ -54,7 +59,9 @@ export class PermissionsGuard implements CanActivate {
}
this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied"));
return this.router.createUrlTree(["/"]);
return canAccessOrgAdmin(org)
? this.router.createUrlTree(["/organizations", org.id])
: this.router.createUrlTree(["/"]);
}
return true;