mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(security): keep ALLOW_LOCAL_AI_PRIVATE_HOSTS=true for the operator's own AI, deprecated
Making `=true` grant nothing would stop a single-user self-host's local model on a patch update. It is deprecated for one release cycle instead, in a narrowed and safe form: it still grants a private host, but only to the operator's own configurations (the instance-wide admin provider and AI settings saved on an admin account), and only through the pinned operator-approved dispatcher, so redirects stay forbidden and metadata, link-local and unspecified addresses stay refused. A non-admin account's own base URL needs an exact origin in AI_PRIVATE_ORIGINS. Provider resolution passes the owning account's role to the client (`operatorTrusted`); the save route and the connection test apply the same rule to the account being configured. The boot summary now says the setting is deprecated and names the replacement, and the admin AI settings page shows the exact AI_PRIVATE_ORIGINS line for the private origins saved on the instance (every account's Local and gateway URL and the admin base URL, scheme, host and port only), so the operator can copy it and drop the old setting.
This commit is contained in:
1 parent
76398c80c2
commit
c86f23c2c0
28 files changed
+863
-55
No files matched your search
@@ -0,0 +1,64 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.39.3 — the admin AI card names the deprecated
|
||||
* `ALLOW_LOCAL_AI_PRIVATE_HOSTS=true` and gives the exact
|
||||
* `AI_PRIVATE_ORIGINS` line to set instead, built from the origins the server
|
||||
* reports in use. Absent when the server reports no such grant.
|
||||
*/
|
||||
|
||||
const state = vi.hoisted(() => ({ data: null as null | object }));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ data: state.data }),
|
||||
useMutation: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
|
||||
}));
|
||||
|
||||
import { AiServerKeySection } from "../ai-server-key-section";
|
||||
|
||||
function render(): string {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<AiServerKeySection />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
const base = {
|
||||
hasKey: false,
|
||||
keyPreview: null,
|
||||
model: "gpt-4o",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
};
|
||||
|
||||
describe("<AiServerKeySection> deprecated private-host notice", () => {
|
||||
it("shows the exact line to set, with every origin in use", () => {
|
||||
state.data = {
|
||||
...base,
|
||||
legacyPrivateHostGrant: {
|
||||
originsInUse: ["http://10.0.0.5:11434", "http://gateway.lan:4000"],
|
||||
},
|
||||
};
|
||||
const html = render();
|
||||
expect(html).toContain('data-slot="admin-ai-legacy-private-hosts"');
|
||||
expect(html).toContain("is deprecated");
|
||||
expect(html).toContain(
|
||||
"AI_PRIVATE_ORIGINS="http://10.0.0.5:11434,http://gateway.lan:4000"",
|
||||
);
|
||||
});
|
||||
|
||||
it("says the setting can go when no saved URL needs it", () => {
|
||||
state.data = { ...base, legacyPrivateHostGrant: { originsInUse: [] } };
|
||||
const html = render();
|
||||
expect(html).toContain("you can remove ALLOW_LOCAL_AI_PRIVATE_HOSTS now");
|
||||
expect(html).not.toContain("admin-ai-legacy-private-hosts-line");
|
||||
});
|
||||
|
||||
it("is absent when the server reports no deprecated grant", () => {
|
||||
state.data = { ...base, legacyPrivateHostGrant: null };
|
||||
expect(render()).not.toContain("admin-ai-legacy-private-hosts");
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { KeyRound, Loader2 } from "lucide-react";
|
||||
import { AlertTriangle, KeyRound, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -49,6 +49,12 @@ interface AiServerKeyResponse {
|
||||
keyPreview: string | null;
|
||||
model: string;
|
||||
baseUrl: string;
|
||||
/**
|
||||
* v1.39.3 — present while the deprecated
|
||||
* `ALLOW_LOCAL_AI_PRIVATE_HOSTS=true` is set: the origins it is standing in
|
||||
* for, to copy into `AI_PRIVATE_ORIGINS`.
|
||||
*/
|
||||
legacyPrivateHostGrant?: { originsInUse: string[] } | null;
|
||||
}
|
||||
|
||||
export function AiServerKeySection() {
|
||||
@@ -120,6 +126,11 @@ export function AiServerKeySection() {
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{settings?.legacyPrivateHostGrant ? (
|
||||
<LegacyPrivateHostNotice
|
||||
originsInUse={settings.legacyPrivateHostGrant.originsInUse}
|
||||
/>
|
||||
) : null}
|
||||
<div className="space-y-2 text-sm">
|
||||
<p>{t("admin.aiServerKey.byokHint")}</p>
|
||||
<p>{t("admin.aiServerKey.consentHint")}</p>
|
||||
@@ -201,3 +212,43 @@ export function AiServerKeySection() {
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.39.3 — the deprecated `ALLOW_LOCAL_AI_PRIVATE_HOSTS=true`, with the exact
|
||||
* line to set instead. The origins come from the saved base URLs on this
|
||||
* instance; the operator copies the line into the server environment.
|
||||
*/
|
||||
function LegacyPrivateHostNotice({ originsInUse }: { originsInUse: string[] }) {
|
||||
const { t } = useTranslations();
|
||||
const line = `AI_PRIVATE_ORIGINS="${originsInUse.join(",")}"`;
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
data-slot="admin-ai-legacy-private-hosts"
|
||||
className="border-warning/40 bg-warning/10 rounded-md border px-3 py-2 text-sm"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 size-4 shrink-0" aria-hidden="true" />
|
||||
<div className="min-w-0 space-y-1">
|
||||
<p className="font-medium">
|
||||
{t("admin.aiServerKey.legacyGrantTitle")}
|
||||
</p>
|
||||
<p>{t("admin.aiServerKey.legacyGrantBody")}</p>
|
||||
{originsInUse.length > 0 ? (
|
||||
<>
|
||||
<p>{t("admin.aiServerKey.legacyGrantOrigins")}</p>
|
||||
<code
|
||||
data-slot="admin-ai-legacy-private-hosts-line"
|
||||
className="bg-muted block overflow-x-auto rounded px-2 py-1 font-mono text-xs break-all select-all"
|
||||
>
|
||||
{line}
|
||||
</code>
|
||||
</>
|
||||
) : (
|
||||
<p>{t("admin.aiServerKey.legacyGrantNoOrigins")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user