fix(permissions): resolve AI module authorization edge cases and test failures

This commit is contained in:
Yash Kumar
2026-09-20 23:50:56 +05:30
parent b5473e63fd
commit cd9edb290b
4 changed files with 76 additions and 26 deletions
@@ -42,6 +42,7 @@ const ENTERPRISE_RESOURCES = [
"auditLog",
"vaultProvider",
"dnsProvider",
"ai",
];
describe("enterpriseOnlyResources set", () => {
+28 -18
View File
@@ -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),
+20 -1
View File
@@ -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",
});
});
+27 -7
View File
@@ -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",