feat: implement board access control (#349)
* feat: implement board access control * fix: deepsource issues * wip: address pull request feedback * chore: address pull request feedback * fix: format issue * test: improve tests * fix: type and lint issue * chore: address pull request feedback * refactor: rename board procedures
This commit is contained in:
188
packages/api/src/router/test/board/board-access.spec.ts
Normal file
188
packages/api/src/router/test/board/board-access.spec.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import * as authShared from "@homarr/auth/shared";
|
||||
import { createId, eq } from "@homarr/db";
|
||||
import { boards, users } from "@homarr/db/schema/sqlite";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { throwIfActionForbiddenAsync } from "../../board/board-access";
|
||||
|
||||
const defaultCreatorId = createId();
|
||||
|
||||
const expectActToBe = async (act: () => Promise<void>, success: boolean) => {
|
||||
if (!success) {
|
||||
await expect(act()).rejects.toThrow("Board not found");
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(act()).resolves.toBeUndefined();
|
||||
};
|
||||
|
||||
// TODO: most of this test can be used for constructBoardPermissions
|
||||
// TODO: the tests for the board-access can be reduced to about 4 tests (as the unit has shrunk)
|
||||
|
||||
describe("throwIfActionForbiddenAsync should check access to board and return boolean", () => {
|
||||
test.each([
|
||||
["full-access" as const, true],
|
||||
["board-change" as const, true],
|
||||
["board-view" as const, true],
|
||||
])(
|
||||
"with permission %s should return %s when hasFullAccess is true",
|
||||
async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: true,
|
||||
hasChangeAccess: false,
|
||||
hasViewAccess: false,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, boardId),
|
||||
permission,
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expectActToBe(act, expectedResult);
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
["full-access" as const, false],
|
||||
["board-change" as const, true],
|
||||
["board-view" as const, true],
|
||||
])(
|
||||
"with permission %s should return %s when hasChangeAccess is true",
|
||||
async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasChangeAccess: true,
|
||||
hasViewAccess: false,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, boardId),
|
||||
permission,
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expectActToBe(act, expectedResult);
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
["full-access" as const, false],
|
||||
["board-change" as const, false],
|
||||
["board-view" as const, true],
|
||||
])(
|
||||
"with permission %s should return %s when hasViewAccess is true",
|
||||
async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasChangeAccess: false,
|
||||
hasViewAccess: true,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, boardId),
|
||||
permission,
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expectActToBe(act, expectedResult);
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
["full-access" as const, false],
|
||||
["board-change" as const, false],
|
||||
["board-view" as const, false],
|
||||
])(
|
||||
"with permission %s should return %s when hasViewAccess is false",
|
||||
async (permission, expectedResult) => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
const spy = vi.spyOn(authShared, "constructBoardPermissions");
|
||||
spy.mockReturnValue({
|
||||
hasFullAccess: false,
|
||||
hasChangeAccess: false,
|
||||
hasViewAccess: false,
|
||||
});
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({
|
||||
id: boardId,
|
||||
name: "test",
|
||||
creatorId: defaultCreatorId,
|
||||
});
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, boardId),
|
||||
permission,
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expectActToBe(act, expectedResult);
|
||||
},
|
||||
);
|
||||
|
||||
test("should throw when board is not found", async () => {
|
||||
// Arrange
|
||||
const db = createDb();
|
||||
|
||||
// Act
|
||||
const act = () =>
|
||||
throwIfActionForbiddenAsync(
|
||||
{ db, session: null },
|
||||
eq(boards.id, createId()),
|
||||
"full-access",
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expect(act()).rejects.toThrow("Board not found");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user