mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 16:30:57 +01:00
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:
@@ -1,6 +1,10 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";
|
||||
import { constructIntegrationPermissions } from "@homarr/auth/shared";
|
||||
import { decryptSecret } from "@homarr/common";
|
||||
import type { Database } from "@homarr/db";
|
||||
import { and, eq, inArray } from "@homarr/db";
|
||||
import { integrations } from "@homarr/db/schema/sqlite";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
@@ -8,12 +12,41 @@ import { z } from "@homarr/validation";
|
||||
|
||||
import { publicProcedure } from "../trpc";
|
||||
|
||||
export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(...kinds: TKind[]) => {
|
||||
type IntegrationAction = "query" | "interact";
|
||||
|
||||
/**
|
||||
* Creates a middleware that provides the integration in the context that is of the specified kinds
|
||||
* @param action query for showing data or interact for mutating data
|
||||
* @param kinds kinds of integrations that are supported
|
||||
* @returns middleware that can be used with trpc
|
||||
* @example publicProcedure.unstable_concat(createOneIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
|
||||
* @throws TRPCError NOT_FOUND if the integration was not found
|
||||
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on the specified integration
|
||||
*/
|
||||
export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(
|
||||
action: IntegrationAction,
|
||||
...kinds: [TKind, ...TKind[]] // Ensure at least one kind is provided
|
||||
) => {
|
||||
return publicProcedure.input(z.object({ integrationId: z.string() })).use(async ({ input, ctx, next }) => {
|
||||
const integration = await ctx.db.query.integrations.findFirst({
|
||||
where: and(eq(integrations.id, input.integrationId), inArray(integrations.kind, kinds)),
|
||||
with: {
|
||||
secrets: true,
|
||||
groupPermissions: true,
|
||||
userPermissions: true,
|
||||
items: {
|
||||
with: {
|
||||
item: {
|
||||
with: {
|
||||
section: {
|
||||
columns: {
|
||||
boardId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,7 +57,16 @@ export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(..
|
||||
});
|
||||
}
|
||||
|
||||
const { secrets, kind, ...rest } = integration;
|
||||
await throwIfActionIsNotAllowedAsync(action, ctx.db, [integration], ctx.session);
|
||||
|
||||
const {
|
||||
secrets,
|
||||
kind,
|
||||
items: _ignore1,
|
||||
groupPermissions: _ignore2,
|
||||
userPermissions: _ignore3,
|
||||
...rest
|
||||
} = integration;
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
@@ -41,7 +83,20 @@ export const createOneIntegrationMiddleware = <TKind extends IntegrationKind>(..
|
||||
});
|
||||
};
|
||||
|
||||
export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(...kinds: TKind[]) => {
|
||||
/**
|
||||
* Creates a middleware that provides the integrations in the context that are of the specified kinds and have the specified item
|
||||
* It also ensures that the user has permission to perform the specified action on the integrations
|
||||
* @param action query for showing data or interact for mutating data
|
||||
* @param kinds kinds of integrations that are supported
|
||||
* @returns middleware that can be used with trpc
|
||||
* @example publicProcedure.unstable_concat(createManyIntegrationMiddleware("query", "piHole", "homeAssistant")).query(...)
|
||||
* @throws TRPCError NOT_FOUND if the integration was not found
|
||||
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on at least one of the specified integrations
|
||||
*/
|
||||
export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(
|
||||
action: IntegrationAction,
|
||||
...kinds: [TKind, ...TKind[]] // Ensure at least one kind is provided
|
||||
) => {
|
||||
return publicProcedure
|
||||
.input(z.object({ integrationIds: z.array(z.string()).min(1) }))
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
@@ -49,7 +104,21 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(.
|
||||
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
|
||||
with: {
|
||||
secrets: true,
|
||||
items: true,
|
||||
items: {
|
||||
with: {
|
||||
item: {
|
||||
with: {
|
||||
section: {
|
||||
columns: {
|
||||
boardId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
userPermissions: true,
|
||||
groupPermissions: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,22 +130,39 @@ export const createManyIntegrationMiddleware = <TKind extends IntegrationKind>(.
|
||||
});
|
||||
}
|
||||
|
||||
await throwIfActionIsNotAllowedAsync(action, ctx.db, dbIntegrations, ctx.session);
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
integrations: dbIntegrations.map(({ secrets, kind, ...rest }) => ({
|
||||
...rest,
|
||||
kind: kind as TKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
})),
|
||||
integrations: dbIntegrations.map(
|
||||
({ secrets, kind, items: _ignore1, groupPermissions: _ignore2, userPermissions: _ignore3, ...rest }) => ({
|
||||
...rest,
|
||||
kind: kind as TKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
}),
|
||||
),
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const createManyIntegrationOfOneItemMiddleware = <TKind extends IntegrationKind>(...kinds: TKind[]) => {
|
||||
/**
|
||||
* Creates a middleware that provides the integrations and their items in the context that are of the specified kinds and have the specified item
|
||||
* It also ensures that the user has permission to perform the specified action on the integrations
|
||||
* @param action query for showing data or interact for mutating data
|
||||
* @param kinds kinds of integrations that are supported
|
||||
* @returns middleware that can be used with trpc
|
||||
* @example publicProcedure.unstable_concat(createManyIntegrationOfOneItemMiddleware("query", "piHole", "homeAssistant")).query(...)
|
||||
* @throws TRPCError NOT_FOUND if the integration for the item was not found
|
||||
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on at least one of the specified integrations
|
||||
*/
|
||||
export const createManyIntegrationOfOneItemMiddleware = <TKind extends IntegrationKind>(
|
||||
action: IntegrationAction,
|
||||
...kinds: [TKind, ...TKind[]] // Ensure at least one kind is provided
|
||||
) => {
|
||||
return publicProcedure
|
||||
.input(z.object({ integrationIds: z.array(z.string()).min(1), itemId: z.string() }))
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
@@ -84,7 +170,21 @@ export const createManyIntegrationOfOneItemMiddleware = <TKind extends Integrati
|
||||
where: and(inArray(integrations.id, input.integrationIds), inArray(integrations.kind, kinds)),
|
||||
with: {
|
||||
secrets: true,
|
||||
items: true,
|
||||
items: {
|
||||
with: {
|
||||
item: {
|
||||
with: {
|
||||
section: {
|
||||
columns: {
|
||||
boardId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
userPermissions: true,
|
||||
groupPermissions: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -96,6 +196,8 @@ export const createManyIntegrationOfOneItemMiddleware = <TKind extends Integrati
|
||||
});
|
||||
}
|
||||
|
||||
await throwIfActionIsNotAllowedAsync(action, ctx.db, dbIntegrations, ctx.session);
|
||||
|
||||
const dbIntegrationWithItem = dbIntegrations.filter((integration) =>
|
||||
integration.items.some((item) => item.itemId === input.itemId),
|
||||
);
|
||||
@@ -109,15 +211,53 @@ export const createManyIntegrationOfOneItemMiddleware = <TKind extends Integrati
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
integrations: dbIntegrationWithItem.map(({ secrets, kind, ...rest }) => ({
|
||||
...rest,
|
||||
kind: kind as TKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
})),
|
||||
integrations: dbIntegrationWithItem.map(
|
||||
({ secrets, kind, groupPermissions: _ignore1, userPermissions: _ignore2, ...rest }) => ({
|
||||
...rest,
|
||||
kind: kind as TKind,
|
||||
decryptedSecrets: secrets.map((secret) => ({
|
||||
...secret,
|
||||
value: decryptSecret(secret.value),
|
||||
})),
|
||||
}),
|
||||
),
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws a TRPCError FORBIDDEN if the user does not have permission to perform the specified action on at least one of the specified integrations
|
||||
* @param action action to perform
|
||||
* @param db db instance
|
||||
* @param integrations integrations to check permissions for
|
||||
* @param session session of the user
|
||||
* @throws TRPCError FORBIDDEN if the user does not have permission to perform the specified action on at least one of the specified integrations
|
||||
*/
|
||||
const throwIfActionIsNotAllowedAsync = async (
|
||||
action: IntegrationAction,
|
||||
db: Database,
|
||||
integrations: Parameters<typeof hasQueryAccessToIntegrationsAsync>[1],
|
||||
session: Session | null,
|
||||
) => {
|
||||
if (action === "interact") {
|
||||
const haveAllInteractAccess = integrations
|
||||
.map((integration) => constructIntegrationPermissions(integration, session))
|
||||
.every(({ hasInteractAccess }) => hasInteractAccess);
|
||||
if (haveAllInteractAccess) return;
|
||||
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User does not have permission to interact with at least one of the specified integrations",
|
||||
});
|
||||
}
|
||||
|
||||
const hasQueryAccess = await hasQueryAccessToIntegrationsAsync(db, integrations, session);
|
||||
|
||||
if (hasQueryAccess) return;
|
||||
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User does not have permission to query at least one of the specified integration",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const calendarRouter = createTRPCRouter({
|
||||
findAllEvents: publicProcedure
|
||||
.unstable_concat(createManyIntegrationOfOneItemMiddleware("sonarr", "radarr", "readarr", "lidarr"))
|
||||
.unstable_concat(createManyIntegrationOfOneItemMiddleware("query", "sonarr", "radarr", "readarr", "lidarr"))
|
||||
.query(async ({ ctx }) => {
|
||||
return await Promise.all(
|
||||
ctx.integrations.flatMap(async (integration) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const dnsHoleRouter = createTRPCRouter({
|
||||
summary: publicProcedure.unstable_concat(createOneIntegrationMiddleware("piHole")).query(async ({ ctx }) => {
|
||||
summary: publicProcedure.unstable_concat(createOneIntegrationMiddleware("query", "piHole")).query(async ({ ctx }) => {
|
||||
const cache = createCacheChannel<DnsHoleSummary>(`dns-hole-summary:${ctx.integration.id}`);
|
||||
|
||||
const { data } = await cache.consumeAsync(async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const mediaServerRouter = createTRPCRouter({
|
||||
getCurrentStreams: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("jellyfin", "plex"))
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", "jellyfin", "plex"))
|
||||
.query(async ({ ctx }) => {
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -22,7 +22,7 @@ export const mediaServerRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
subscribeToCurrentStreams: publicProcedure
|
||||
.unstable_concat(createManyIntegrationMiddleware("jellyfin", "plex"))
|
||||
.unstable_concat(createManyIntegrationMiddleware("query", "jellyfin", "plex"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{ integrationId: string; data: StreamSession[] }>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
|
||||
@@ -26,14 +26,14 @@ export const smartHomeRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
switchEntity: publicProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("homeAssistant"))
|
||||
.unstable_concat(createOneIntegrationMiddleware("interact", "homeAssistant"))
|
||||
.input(z.object({ entityId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const client = new HomeAssistantIntegration(ctx.integration);
|
||||
return await client.triggerToggleAsync(input.entityId);
|
||||
}),
|
||||
executeAutomation: publicProcedure
|
||||
.unstable_concat(createOneIntegrationMiddleware("homeAssistant"))
|
||||
.unstable_concat(createOneIntegrationMiddleware("interact", "homeAssistant"))
|
||||
.input(z.object({ automationId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const client = new HomeAssistantIntegration(ctx.integration);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"./next": "./next.ts",
|
||||
"./security": "./security.ts",
|
||||
"./client": "./client.ts",
|
||||
"./server": "./server.ts",
|
||||
"./shared": "./shared.ts",
|
||||
"./env.mjs": "./env.mjs"
|
||||
},
|
||||
|
||||
124
packages/auth/permissions/integration-query-permissions.ts
Normal file
124
packages/auth/permissions/integration-query-permissions.ts
Normal 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)),
|
||||
);
|
||||
};
|
||||
@@ -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
1
packages/auth/server.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { hasQueryAccessToIntegrationsAsync } from "./permissions/integration-query-permissions";
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user