feat: add integration access check to middlewares (#756)

* feat: add integration access check to middlewares

* fix: format issues

* fix: remove group and user permissions and items from context

* refactor: move action check to seperate function
This commit is contained in:
Meier Lukas
2024-07-08 17:39:36 +02:00
committed by GitHub
parent 8d42ca8b5e
commit 46943b147a
11 changed files with 966 additions and 29 deletions

View File

@@ -14,12 +14,15 @@ export const getCurrentUserPermissionsAsync = async (db: Database, userId: strin
where: eq(groupMembers.userId, userId),
});
const groupIds = dbGroupMembers.map((groupMember) => groupMember.groupId);
if (groupIds.length === 0) return [];
const dbGroupPermissions = await db
.selectDistinct({
permission: groupPermissions.permission,
})
.from(groupPermissions)
.where(groupIds.length > 0 ? inArray(groupPermissions.groupId, groupIds) : undefined);
.where(inArray(groupPermissions.groupId, groupIds));
const permissionKeys = dbGroupPermissions.map(({ permission }) => permission);
return getPermissionsWithChildren(permissionKeys);

View File

@@ -7,6 +7,7 @@
"./next": "./next.ts",
"./security": "./security.ts",
"./client": "./client.ts",
"./server": "./server.ts",
"./shared": "./shared.ts",
"./env.mjs": "./env.mjs"
},

View File

@@ -0,0 +1,124 @@
import type { Session } from "next-auth";
import type { Database } from "@homarr/db";
import { and, eq, inArray, or } from "@homarr/db";
import { boards, boardUserPermissions, groupMembers } from "@homarr/db/schema/sqlite";
import type { IntegrationPermission } from "@homarr/definitions";
import { constructIntegrationPermissions } from "./integration-permissions";
interface Integration {
id: string;
items: {
item: {
section: {
boardId: string;
};
};
}[];
userPermissions: {
permission: IntegrationPermission;
}[];
groupPermissions: {
permission: IntegrationPermission;
}[];
}
export const hasQueryAccessToIntegrationsAsync = async (
db: Database,
integrations: Integration[],
session: Session | null,
) => {
// If the user has board-view-all and every integration has at least one item that is placed on a board he has access.
if (
session?.user.permissions.includes("board-view-all") &&
integrations.every((integration) => integration.items.length >= 1)
) {
return true;
}
const integrationsWithUseAccess = integrations.filter(
(integration) => constructIntegrationPermissions(integration, session).hasUseAccess,
);
// If the user has use access to all integrations, he has access.
if (integrationsWithUseAccess.length === integrations.length) {
return true;
}
const integrationsWithoutUseAccessAndWithoutBoardViewAllAccess = integrations
.filter((integration) => !integrationsWithUseAccess.includes(integration))
.filter((integration) => !(session?.user.permissions.includes("board-view-all") && integration.items.length >= 1));
if (integrationsWithoutUseAccessAndWithoutBoardViewAllAccess.length === 0) {
return true;
}
const integrationsWithBoardIds = integrationsWithoutUseAccessAndWithoutBoardViewAllAccess.map((integration) => ({
id: integration.id,
anyOfBoardIds: integration.items.map(({ item }) => item.section.boardId),
}));
const permissionsOfCurrentUserWhenPresent = await db.query.boardUserPermissions.findMany({
where: eq(boardUserPermissions.userId, session?.user.id ?? ""),
});
// If for each integration the user has access to at least of of it's present boards, he has access.
if (
checkEveryIntegrationContainsSomeBoardIdIncludedInBoardsWithAccess(
integrationsWithBoardIds,
permissionsOfCurrentUserWhenPresent.map(({ boardId }) => boardId),
)
) {
return true;
}
const permissionsOfCurrentUserGroupsWhenPresent = await db.query.groupMembers.findMany({
where: eq(groupMembers.userId, session?.user.id ?? ""),
with: {
group: {
with: {
boardPermissions: {},
},
},
},
});
const boardIdsWithPermission = permissionsOfCurrentUserWhenPresent
.map((permission) => permission.boardId)
.concat(
permissionsOfCurrentUserGroupsWhenPresent
.map((groupMember) => groupMember.group.boardPermissions.map((permission) => permission.boardId))
.flat(),
);
// If for each integration the user has access to at least of of it's present boards, he has access.
if (
checkEveryIntegrationContainsSomeBoardIdIncludedInBoardsWithAccess(integrationsWithBoardIds, boardIdsWithPermission)
) {
return true;
}
const relevantBoardIds = [...new Set(integrationsWithBoardIds.map(({ anyOfBoardIds }) => anyOfBoardIds).flat())];
const publicBoardsOrBoardsWhereCurrentUserIsOwner = await db.query.boards.findMany({
where: and(
or(eq(boards.isPublic, true), eq(boards.creatorId, session?.user.id ?? "")),
inArray(boards.id, relevantBoardIds),
),
});
const boardsWithAccess = boardIdsWithPermission.concat(
publicBoardsOrBoardsWhereCurrentUserIsOwner.map(({ id }) => id),
);
// If for each integration the user has access to at least of of it's present boards, he has access.
return checkEveryIntegrationContainsSomeBoardIdIncludedInBoardsWithAccess(integrationsWithBoardIds, boardsWithAccess);
};
const checkEveryIntegrationContainsSomeBoardIdIncludedInBoardsWithAccess = (
integration: { id: string; anyOfBoardIds: string[] }[],
boardIdsWithAccess: string[],
) => {
return integration.every(({ anyOfBoardIds }) =>
anyOfBoardIds.some((boardId) => boardIdsWithAccess.includes(boardId)),
);
};

View File

@@ -0,0 +1,639 @@
import type { Session } from "next-auth";
import { describe, expect, test, vi } from "vitest";
import type { InferInsertModel } from "@homarr/db";
import { createId } from "@homarr/db";
import {
boardGroupPermissions,
boards,
boardUserPermissions,
groupMembers,
groups,
users,
} from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
import * as integrationPermissions from "../integration-permissions";
import { hasQueryAccessToIntegrationsAsync } from "../integration-query-permissions";
const createSession = (user: Partial<Session["user"]>): Session => ({
user: {
id: "1",
permissions: [],
...user,
},
expires: new Date().toISOString(),
});
describe("hasQueryAccessToIntegrationsAsync should check if the user has query access to the specified integrations", () => {
test("should return true if the user has the board-view-all permission and the integrations are used anywhere", async () => {
// Arrange
const db = createDb();
const session = createSession({
permissions: ["board-view-all"],
});
const integrations = [
{
id: "1",
items: [{ item: { section: { boardId: "1" } } }],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [{ item: { section: { boardId: "2" } } }],
userPermissions: [],
groupPermissions: [],
},
];
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(true);
});
test("should return true if the user has the board-view-all permission, the first integration is used and the second one he has use access", async () => {
// Arrange
const db = createDb();
const session = createSession({
permissions: ["board-view-all"],
});
const spy = vi.spyOn(integrationPermissions, "constructIntegrationPermissions");
spy.mockReturnValue({
hasFullAccess: false,
hasInteractAccess: false,
hasUseAccess: true,
});
const integrations = [
{
id: "1",
items: [{ item: { section: { boardId: "1" } } }],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [],
userPermissions: [],
groupPermissions: [],
},
];
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(true);
});
test("should return true if the user has use access to all integrations", async () => {
// Arrange
const db = createDb();
const session = createSession({});
const spy = vi.spyOn(integrationPermissions, "constructIntegrationPermissions");
spy.mockReturnValue({
hasFullAccess: false,
hasInteractAccess: false,
hasUseAccess: true,
});
const integrations = [
{
id: "1",
items: [],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [],
userPermissions: [],
groupPermissions: [],
},
];
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(true);
});
test("should return true if the user has user permission to access to at least one board of each integration", async () => {
// Arrange
const db = createDb();
const session = createSession({});
await db.insert(users).values({ id: session.user.id });
const spy = vi.spyOn(integrationPermissions, "constructIntegrationPermissions");
spy.mockReturnValue({
hasFullAccess: false,
hasInteractAccess: false,
hasUseAccess: false,
});
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
{
item: {
section: {
boardId: "2",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
await db.insert(boards).values(createMockBoard({ id: "1" }));
await db.insert(boardUserPermissions).values({ userId: session.user.id, boardId: "1", permission: "view" });
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(true);
});
test("should return false if the user has user permission to access board of first integration but not of second one", async () => {
// Arrange
const db = createDb();
const session = createSession({});
await db.insert(users).values({ id: session.user.id });
const spy = vi.spyOn(integrationPermissions, "constructIntegrationPermissions");
spy.mockReturnValue({
hasFullAccess: false,
hasInteractAccess: false,
hasUseAccess: false,
});
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "2",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
await db.insert(boards).values(createMockBoard({ id: "1" }));
await db.insert(boardUserPermissions).values({ userId: session.user.id, boardId: "1", permission: "view" });
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(false);
});
test("should return true if the user has group permission to access to at least one board of each integration", async () => {
// Arrange
const db = createDb();
const session = createSession({});
await db.insert(users).values({ id: session.user.id });
const spy = vi.spyOn(integrationPermissions, "constructIntegrationPermissions");
spy.mockReturnValue({
hasFullAccess: false,
hasInteractAccess: false,
hasUseAccess: false,
});
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
{
item: {
section: {
boardId: "2",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
await db.insert(boards).values(createMockBoard({ id: "1" }));
await db.insert(groups).values({ id: "1", name: "" });
await db.insert(groupMembers).values({ userId: session.user.id, groupId: "1" });
await db.insert(boardGroupPermissions).values({ groupId: "1", boardId: "1", permission: "view" });
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(true);
});
test("should return false if the user has group permission to access board of first integration but not of second one", async () => {
// Arrange
const db = createDb();
const session = createSession({});
await db.insert(users).values({ id: session.user.id });
const spy = vi.spyOn(integrationPermissions, "constructIntegrationPermissions");
spy.mockReturnValue({
hasFullAccess: false,
hasInteractAccess: false,
hasUseAccess: false,
});
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "2",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
await db.insert(boards).values(createMockBoard({ id: "1" }));
await db.insert(groups).values({ id: "1", name: "" });
await db.insert(groupMembers).values({ userId: session.user.id, groupId: "1" });
await db.insert(boardGroupPermissions).values({ groupId: "1", boardId: "1", permission: "view" });
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(false);
});
test("should return true if the user has user permission to access first board and group permission to access second one", async () => {
// Arrange
const db = createDb();
const session = createSession({});
await db.insert(users).values({ id: session.user.id });
const spy = vi.spyOn(integrationPermissions, "constructIntegrationPermissions");
spy.mockReturnValue({
hasFullAccess: false,
hasInteractAccess: false,
hasUseAccess: false,
});
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "2",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
await db.insert(boards).values(createMockBoard({ id: "1" }));
await db.insert(boards).values(createMockBoard({ id: "2" }));
await db.insert(groups).values({ id: "1", name: "" });
await db.insert(groupMembers).values({ userId: session.user.id, groupId: "1" });
await db.insert(boardGroupPermissions).values({ groupId: "1", boardId: "2", permission: "view" });
await db.insert(boardUserPermissions).values({ userId: session.user.id, boardId: "1", permission: "view" });
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(true);
});
test("should return true if one of the boards the integration is used is public", async () => {
// Arrange
const db = createDb();
const session = createSession({});
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "2",
},
},
},
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
await db.insert(boards).values(createMockBoard({ id: "1", isPublic: true }));
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(true);
});
test("should return true if the user is creator of the board the integration is used", async () => {
// Arrange
const db = createDb();
const session = createSession({});
await db.insert(users).values({ id: session.user.id });
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "2",
},
},
},
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
await db.insert(boards).values(createMockBoard({ id: "1", creatorId: session.user.id }));
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(true);
});
test("should return false if the user has no access to any of the integrations", async () => {
// Arrange
const db = createDb();
const session = createSession({});
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "2",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
// Assert
expect(result).toBe(false);
});
test("should return false if the user is anonymous and the board is not public", async () => {
// Arrange
const db = createDb();
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "2",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
await db.insert(boards).values(createMockBoard({ id: "1" }));
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, null);
// Assert
expect(result).toBe(false);
});
test("should return true if the user is anonymous and the board is public", async () => {
// Arrange
const db = createDb();
const integrations = [
{
id: "1",
items: [
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
{
id: "2",
items: [
{
item: {
section: {
boardId: "2",
},
},
},
{
item: {
section: {
boardId: "1",
},
},
},
],
userPermissions: [],
groupPermissions: [],
},
];
await db.insert(boards).values(createMockBoard({ id: "1", isPublic: true }));
// Act
const result = await hasQueryAccessToIntegrationsAsync(db, integrations, null);
// Assert
expect(result).toBe(true);
});
});
const createMockBoard = (board: Partial<InferInsertModel<typeof boards>>): InferInsertModel<typeof boards> => ({
id: createId(),
name: board.id ?? createId(),
...board,
});

1
packages/auth/server.ts Normal file
View File

@@ -0,0 +1 @@
export { hasQueryAccessToIntegrationsAsync } from "./permissions/integration-query-permissions";

View File

@@ -17,6 +17,14 @@ describe("getCurrentUserPermissions", () => {
test("should return empty permissions when non existing user requested", async () => {
const db = createDb();
await db.insert(groups).values({
id: "2",
name: "test",
});
await db.insert(groupPermissions).values({
groupId: "2",
permission: "admin",
});
await db.insert(users).values({
id: "2",
});
@@ -25,6 +33,27 @@ describe("getCurrentUserPermissions", () => {
const result = await getCurrentUserPermissionsAsync(db, userId);
expect(result).toEqual([]);
});
test("should return empty permissions when user has no groups", async () => {
const db = createDb();
const userId = "1";
await db.insert(groups).values({
id: "2",
name: "test",
});
await db.insert(groupPermissions).values({
groupId: "2",
permission: "admin",
});
await db.insert(users).values({
id: userId,
});
const result = await getCurrentUserPermissionsAsync(db, userId);
expect(result).toEqual([]);
});
test("should return permissions for user", async () => {
const db = createDb();
const getPermissionsWithChildrenMock = vi