mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 21:31:11 +00:00
fix(permissions): resolve AI module authorization edge cases and test failures
This commit is contained in:
@@ -42,6 +42,7 @@ const ENTERPRISE_RESOURCES = [
|
||||
"auditLog",
|
||||
"vaultProvider",
|
||||
"dnsProvider",
|
||||
"ai",
|
||||
];
|
||||
|
||||
describe("enterpriseOnlyResources set", () => {
|
||||
|
||||
@@ -38,18 +38,22 @@ import { slugify } from "@/lib/slug";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
withAnyPermission,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { generatePassword } from "@/templates/utils";
|
||||
|
||||
export const aiRouter = createTRPCRouter({
|
||||
one: withPermission("ai", "read")
|
||||
one: withAnyPermission("ai", ["read", "update"])
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getAiSettingById(input.aiId);
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await getAiSettingById(
|
||||
input.aiId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}),
|
||||
|
||||
getModels: withPermission("ai", "read")
|
||||
getModels: withAnyPermission("ai", ["read", "create", "update"])
|
||||
.input(z.object({ apiUrl: z.string().min(1), apiKey: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
try {
|
||||
@@ -197,17 +201,27 @@ export const aiRouter = createTRPCRouter({
|
||||
|
||||
get: withPermission("ai", "read")
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
return await getAiSettingById(input.aiId);
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await getAiSettingById(
|
||||
input.aiId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}),
|
||||
|
||||
delete: withPermission("ai", "delete")
|
||||
.input(z.object({ aiId: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return await deleteAiSettings(input.aiId);
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
return await deleteAiSettings(
|
||||
input.aiId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
}),
|
||||
|
||||
getCustomProviders: withPermission("ai", "read").query(async ({ ctx }) => {
|
||||
getCustomProviders: withAnyPermission("ai", [
|
||||
"read",
|
||||
"create",
|
||||
"update",
|
||||
]).query(async ({ ctx }) => {
|
||||
return await getCustomAiProviders(ctx.session.activeOrganizationId);
|
||||
}),
|
||||
|
||||
@@ -239,7 +253,10 @@ export const aiRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const aiSettings = await getAiSettingById(input.aiId);
|
||||
const aiSettings = await getAiSettingById(
|
||||
input.aiId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (!aiSettings?.isEnabled) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -247,13 +264,6 @@ export const aiRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (aiSettings.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Access denied",
|
||||
});
|
||||
}
|
||||
|
||||
const provider = selectAIProvider(aiSettings);
|
||||
const model = provider(aiSettings.model);
|
||||
|
||||
@@ -287,7 +297,7 @@ ${input.logs}`,
|
||||
}
|
||||
}),
|
||||
|
||||
testConnection: withPermission("ai", "create")
|
||||
testConnection: withAnyPermission("ai", ["create", "update"])
|
||||
.input(
|
||||
z.object({
|
||||
apiUrl: z.string().min(1),
|
||||
|
||||
@@ -12,7 +12,10 @@ import { db } from "@dokploy/server/db";
|
||||
import { hasValidLicense } from "@dokploy/server/index";
|
||||
import type { statements } from "@dokploy/server/lib/access-control";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { checkPermission } from "@dokploy/server/services/permission";
|
||||
import {
|
||||
checkPermission,
|
||||
hasPermission,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import type { OpenApiMeta } from "@dokploy/trpc-openapi";
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
@@ -263,3 +266,19 @@ export const withPermission = <R extends Resource>(
|
||||
await checkPermission(ctx, { [resource]: [action] } as any);
|
||||
return next();
|
||||
});
|
||||
|
||||
export const withAnyPermission = <R extends Resource>(
|
||||
resource: R,
|
||||
actions: ActionOf<R>[],
|
||||
) =>
|
||||
protectedProcedure.use(async ({ ctx, next }) => {
|
||||
for (const action of actions) {
|
||||
if (await hasPermission(ctx, { [resource]: [action] } as any)) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Permission denied",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { aiCustomProviderSchema } from "@dokploy/server/db/schema/ai";
|
||||
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { generateText, Output } from "ai";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { findServerById } from "./server";
|
||||
@@ -36,9 +36,12 @@ export const getAiSettingsByOrganizationId = async (organizationId: string) => {
|
||||
return aiSettings;
|
||||
};
|
||||
|
||||
export const getAiSettingById = async (aiId: string) => {
|
||||
export const getAiSettingById = async (
|
||||
aiId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
const aiSetting = await db.query.ai.findFirst({
|
||||
where: eq(ai.aiId, aiId),
|
||||
where: and(eq(ai.aiId, aiId), eq(ai.organizationId, organizationId)),
|
||||
});
|
||||
if (!aiSetting) {
|
||||
throw new TRPCError({
|
||||
@@ -100,6 +103,18 @@ const normalizeApiUrl = (url: string) => url.trim().replace(/\/+$/, "");
|
||||
export const saveAiSettings = async (organizationId: string, settings: any) => {
|
||||
const aiId = settings.aiId;
|
||||
|
||||
if (aiId) {
|
||||
const existing = await db.query.ai.findFirst({
|
||||
where: eq(ai.aiId, aiId),
|
||||
});
|
||||
if (existing && existing.organizationId !== organizationId) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You do not have permission to modify this AI configuration",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.apiUrl) {
|
||||
const customProviders = await getCustomAiProviders(organizationId);
|
||||
if (customProviders.length > 0) {
|
||||
@@ -132,8 +147,13 @@ export const saveAiSettings = async (organizationId: string, settings: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteAiSettings = async (aiId: string) => {
|
||||
return db.delete(ai).where(eq(ai.aiId, aiId));
|
||||
export const deleteAiSettings = async (
|
||||
aiId: string,
|
||||
organizationId: string,
|
||||
) => {
|
||||
return db
|
||||
.delete(ai)
|
||||
.where(and(eq(ai.aiId, aiId), eq(ai.organizationId, organizationId)));
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@@ -144,13 +164,13 @@ interface Props {
|
||||
}
|
||||
|
||||
export const suggestVariants = async ({
|
||||
organizationId: _organizationId,
|
||||
organizationId,
|
||||
aiId,
|
||||
input,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
try {
|
||||
const aiSettings = await getAiSettingById(aiId);
|
||||
const aiSettings = await getAiSettingById(aiId, organizationId);
|
||||
if (!aiSettings || !aiSettings.isEnabled) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
|
||||
Reference in New Issue
Block a user