feat(settings): restrict AI readiness checks to admins
This commit is contained in:
@@ -79,6 +79,15 @@ Reasoning:
|
|||||||
|
|
||||||
- system role defaults define the effective permission model and therefore belong to the smallest operational audience
|
- system role defaults define the effective permission model and therefore belong to the smallest operational audience
|
||||||
|
|
||||||
|
### `packages/api/src/router/settings.ts`
|
||||||
|
|
||||||
|
- `getSystemSettings`, `updateSystemSettings`, connection tests, `getAiConfigured`: `admin-only`
|
||||||
|
|
||||||
|
Reasoning:
|
||||||
|
|
||||||
|
- even the boolean AI readiness check leaks whether admin-managed infrastructure is wired and available
|
||||||
|
- the route has no current web consumer outside admin operations, so narrowing it does not block normal user workflows
|
||||||
|
|
||||||
### `packages/api/src/router/country.ts`
|
### `packages/api/src/router/country.ts`
|
||||||
|
|
||||||
- `list`, `resolveByIdentifier`, `getCityById`: `authenticated-safe-lookup`
|
- `list`, `resolveByIdentifier`, `getCityById`: `authenticated-safe-lookup`
|
||||||
|
|||||||
@@ -626,11 +626,11 @@ describe("assistant router tool gating", () => {
|
|||||||
expect(userNames).not.toContain("get_system_settings");
|
expect(userNames).not.toContain("get_system_settings");
|
||||||
expect(userNames).not.toContain("update_system_settings");
|
expect(userNames).not.toContain("update_system_settings");
|
||||||
expect(userNames).not.toContain("test_ai_connection");
|
expect(userNames).not.toContain("test_ai_connection");
|
||||||
|
expect(userNames).not.toContain("get_ai_configured");
|
||||||
expect(userNames).not.toContain("list_system_role_configs");
|
expect(userNames).not.toContain("list_system_role_configs");
|
||||||
expect(userNames).not.toContain("update_system_role_config");
|
expect(userNames).not.toContain("update_system_role_config");
|
||||||
expect(userNames).not.toContain("list_webhooks");
|
expect(userNames).not.toContain("list_webhooks");
|
||||||
expect(userNames).not.toContain("create_webhook");
|
expect(userNames).not.toContain("create_webhook");
|
||||||
expect(userNames).toContain("get_ai_configured");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps holiday calendar catalog tools admin-only while leaving preview available", () => {
|
it("keeps holiday calendar catalog tools admin-only while leaving preview available", () => {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { SystemRole } from "@capakraken/shared";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { settingsRouter } from "../router/settings.js";
|
||||||
|
import { createCallerFactory } from "../trpc.js";
|
||||||
|
|
||||||
|
function createProtectedContext(
|
||||||
|
db: Record<string, unknown>,
|
||||||
|
systemRole: SystemRole,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
session: {
|
||||||
|
user: { email: "user@example.com", name: "User", image: null },
|
||||||
|
expires: "2099-01-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
db: db as never,
|
||||||
|
dbUser: {
|
||||||
|
id: "user_1",
|
||||||
|
systemRole,
|
||||||
|
permissionOverrides: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("settings router authorization", () => {
|
||||||
|
it("forbids non-admin users from reading AI configuration status", async () => {
|
||||||
|
const findUnique = vi.fn();
|
||||||
|
const caller = createCallerFactory(settingsRouter)(createProtectedContext({
|
||||||
|
systemSettings: {
|
||||||
|
findUnique,
|
||||||
|
},
|
||||||
|
}, SystemRole.USER));
|
||||||
|
|
||||||
|
await expect(caller.getAiConfigured()).rejects.toMatchObject({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Admin role required",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findUnique).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows admins to read AI configuration status", async () => {
|
||||||
|
const findUnique = vi.fn().mockResolvedValue({
|
||||||
|
aiProvider: "azure",
|
||||||
|
azureOpenAiEndpoint: "https://example.openai.azure.com",
|
||||||
|
azureOpenAiDeployment: "gpt-4o",
|
||||||
|
azureOpenAiApiKey: "secret",
|
||||||
|
});
|
||||||
|
const caller = createCallerFactory(settingsRouter)(createProtectedContext({
|
||||||
|
systemSettings: {
|
||||||
|
findUnique,
|
||||||
|
},
|
||||||
|
}, SystemRole.ADMIN));
|
||||||
|
|
||||||
|
const result = await caller.getAiConfigured();
|
||||||
|
|
||||||
|
expect(result).toEqual({ configured: true });
|
||||||
|
expect(findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { id: "singleton" },
|
||||||
|
select: {
|
||||||
|
aiProvider: true,
|
||||||
|
azureOpenAiEndpoint: true,
|
||||||
|
azureOpenAiDeployment: true,
|
||||||
|
azureOpenAiApiKey: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -344,6 +344,7 @@ const ADMIN_ONLY_TOOLS = new Set([
|
|||||||
"commit_dispo_import_batch",
|
"commit_dispo_import_batch",
|
||||||
"get_system_settings",
|
"get_system_settings",
|
||||||
"update_system_settings",
|
"update_system_settings",
|
||||||
|
"get_ai_configured",
|
||||||
"test_ai_connection",
|
"test_ai_connection",
|
||||||
"test_smtp_connection",
|
"test_smtp_connection",
|
||||||
"test_gemini_connection",
|
"test_gemini_connection",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc.js";
|
import { adminProcedure, createTRPCRouter } from "../trpc.js";
|
||||||
import { createAiClient, isAiConfigured, parseAiError } from "../ai-client.js";
|
import { createAiClient, isAiConfigured, parseAiError } from "../ai-client.js";
|
||||||
import { DEFAULT_SUMMARY_PROMPT } from "./resource.js";
|
import { DEFAULT_SUMMARY_PROMPT } from "./resource.js";
|
||||||
import { VALUE_SCORE_WEIGHTS } from "@capakraken/shared";
|
import { VALUE_SCORE_WEIGHTS } from "@capakraken/shared";
|
||||||
@@ -361,7 +361,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getAiConfigured: protectedProcedure.query(async ({ ctx }) => {
|
getAiConfigured: adminProcedure.query(async ({ ctx }) => {
|
||||||
const settings = await ctx.db.systemSettings.findUnique({
|
const settings = await ctx.db.systemSettings.findUnique({
|
||||||
where: { id: "singleton" },
|
where: { id: "singleton" },
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
Reference in New Issue
Block a user