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",
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user