refactor(settings): adopt environment-only runtime secret flow
This commit is contained in:
@@ -618,10 +618,12 @@ describe("assistant router tool gating", () => {
|
||||
const adminNames = getToolNames([], SystemRole.ADMIN);
|
||||
const userNames = getToolNames([], SystemRole.USER);
|
||||
|
||||
const managerNames = getToolNames([], SystemRole.MANAGER);
|
||||
expect(adminNames).toContain("get_system_settings");
|
||||
expect(adminNames).toContain("update_system_settings");
|
||||
expect(adminNames).toContain("test_ai_connection");
|
||||
expect(adminNames).toContain("test_smtp_connection");
|
||||
expect(adminNames).toContain("clear_stored_runtime_secrets");
|
||||
expect(adminNames).toContain("test_gemini_connection");
|
||||
expect(adminNames).toContain("list_system_role_configs");
|
||||
expect(adminNames).toContain("update_system_role_config");
|
||||
@@ -632,12 +634,22 @@ describe("assistant router tool gating", () => {
|
||||
expect(adminNames).toContain("delete_webhook");
|
||||
expect(adminNames).toContain("test_webhook");
|
||||
expect(adminNames).toContain("get_ai_configured");
|
||||
expect(adminNames).toContain("list_system_role_configs");
|
||||
|
||||
expect(managerNames).not.toContain("get_system_settings");
|
||||
expect(managerNames).not.toContain("update_system_settings");
|
||||
expect(managerNames).not.toContain("clear_stored_runtime_secrets");
|
||||
expect(managerNames).not.toContain("test_ai_connection");
|
||||
expect(managerNames).not.toContain("get_ai_configured");
|
||||
expect(managerNames).not.toContain("list_system_role_configs");
|
||||
expect(managerNames).not.toContain("update_system_role_config");
|
||||
expect(managerNames).not.toContain("list_webhooks");
|
||||
expect(managerNames).not.toContain("create_webhook");
|
||||
|
||||
expect(userNames).not.toContain("get_system_settings");
|
||||
expect(userNames).not.toContain("update_system_settings");
|
||||
expect(userNames).not.toContain("test_ai_connection");
|
||||
expect(userNames).not.toContain("get_ai_configured");
|
||||
expect(userNames).not.toContain("clear_stored_runtime_secrets");
|
||||
expect(userNames).not.toContain("list_system_role_configs");
|
||||
expect(userNames).not.toContain("update_system_role_config");
|
||||
expect(userNames).not.toContain("list_webhooks");
|
||||
@@ -996,6 +1008,8 @@ describe("assistant router tool gating", () => {
|
||||
expect(toolDescriptions.get("update_system_settings")).toContain("Always confirm first");
|
||||
expect(toolDescriptions.get("get_ai_configured")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("list_system_role_configs")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("update_system_settings")).toContain("Runtime secrets must be provisioned");
|
||||
expect(toolDescriptions.get("clear_stored_runtime_secrets")).toContain("deployment secret management");
|
||||
expect(toolDescriptions.get("update_system_role_config")).toContain("Admin role");
|
||||
expect(toolDescriptions.get("list_webhooks")).toContain("Secrets are masked");
|
||||
expect(toolDescriptions.get("create_webhook")).toContain("Always confirm first");
|
||||
|
||||
@@ -150,6 +150,7 @@ function createToolContext(
|
||||
describe("assistant import/export and dispo tools", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
apiRateLimiter.reset();
|
||||
totpValidateMock.mockReset();
|
||||
vi.mocked(approveEstimateVersion).mockReset();
|
||||
@@ -288,6 +289,67 @@ describe("assistant import/export and dispo tools", () => {
|
||||
expect(JSON.parse(result.content)).toEqual({ configured: true });
|
||||
});
|
||||
|
||||
it("treats environment-backed AI configuration as configured for assistant checks", async () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "env-secret");
|
||||
|
||||
const ctx = createToolContext(
|
||||
{
|
||||
systemSettings: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
azureOpenAiApiKey: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{ userRole: SystemRole.USER },
|
||||
);
|
||||
|
||||
const result = await executeTool("get_ai_configured", "{}", ctx);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({ configured: true });
|
||||
});
|
||||
|
||||
it("clears legacy runtime secrets through the real settings router path", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
azureOpenAiApiKey: "db-openai",
|
||||
azureDalleApiKey: null,
|
||||
geminiApiKey: "db-gemini",
|
||||
smtpPassword: null,
|
||||
anonymizationSeed: "db-seed",
|
||||
});
|
||||
const update = vi.fn().mockResolvedValue({ id: "singleton" });
|
||||
const auditCreate = vi.fn().mockResolvedValue(undefined);
|
||||
const ctx = createToolContext(
|
||||
{
|
||||
systemSettings: {
|
||||
findUnique,
|
||||
update,
|
||||
},
|
||||
auditLog: {
|
||||
create: auditCreate,
|
||||
},
|
||||
},
|
||||
{ userRole: SystemRole.ADMIN },
|
||||
);
|
||||
|
||||
const result = await executeTool("clear_stored_runtime_secrets", "{}", ctx);
|
||||
|
||||
expect(JSON.parse(result.content)).toEqual({
|
||||
ok: true,
|
||||
clearedFields: ["azureOpenAiApiKey", "geminiApiKey", "anonymizationSeed"],
|
||||
});
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: "singleton" },
|
||||
data: {
|
||||
azureOpenAiApiKey: null,
|
||||
geminiApiKey: null,
|
||||
anonymizationSeed: null,
|
||||
},
|
||||
});
|
||||
expect(auditCreate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("masks webhook secrets in assistant responses", async () => {
|
||||
const ctx = createToolContext(
|
||||
{
|
||||
|
||||
@@ -118,6 +118,9 @@ describe("runtime config hardening", () => {
|
||||
expect(result.hasApiKey).toBe(true);
|
||||
expect(result.hasSmtpPassword).toBe(true);
|
||||
expect(result.hasGeminiApiKey).toBe(true);
|
||||
expect(result.runtimeSecrets.azureOpenAiApiKey.activeSource).toBe("environment");
|
||||
expect(result.runtimeSecrets.smtpPassword.activeSource).toBe("environment");
|
||||
expect(result.legacyStoredSecretFields).toEqual([]);
|
||||
});
|
||||
|
||||
it("prefers environment API keys during AI connection tests", async () => {
|
||||
@@ -150,4 +153,81 @@ describe("runtime config hardening", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not persist incoming secret fields through updateSystemSettings", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue(null);
|
||||
const upsert = vi.fn().mockResolvedValue({});
|
||||
const caller = createAdminCaller({
|
||||
systemSettings: {
|
||||
findUnique,
|
||||
upsert,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await caller.updateSystemSettings({
|
||||
azureOpenAiApiKey: "sk-should-not-store",
|
||||
smtpPassword: "smtp-should-not-store",
|
||||
geminiApiKey: "gemini-should-not-store",
|
||||
azureDalleApiKey: "dalle-should-not-store",
|
||||
anonymizationSeed: "seed-should-not-store",
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
ignoredSecretFields: [
|
||||
"azureOpenAiApiKey",
|
||||
"smtpPassword",
|
||||
"anonymizationSeed",
|
||||
"azureDalleApiKey",
|
||||
"geminiApiKey",
|
||||
],
|
||||
secretStorageMode: "environment-only",
|
||||
});
|
||||
expect(upsert).toHaveBeenCalledWith({
|
||||
where: { id: "singleton" },
|
||||
create: {
|
||||
id: "singleton",
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
},
|
||||
update: {
|
||||
aiProvider: "openai",
|
||||
azureOpenAiDeployment: "gpt-4o-mini",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("can clear legacy runtime secrets from database storage", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
azureOpenAiApiKey: "db-key",
|
||||
azureDalleApiKey: null,
|
||||
geminiApiKey: "db-gemini",
|
||||
smtpPassword: "db-smtp",
|
||||
anonymizationSeed: null,
|
||||
});
|
||||
const update = vi.fn().mockResolvedValue({});
|
||||
const caller = createAdminCaller({
|
||||
systemSettings: {
|
||||
findUnique,
|
||||
update,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await caller.clearStoredRuntimeSecrets();
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
clearedFields: ["azureOpenAiApiKey", "geminiApiKey", "smtpPassword"],
|
||||
});
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: "singleton" },
|
||||
data: {
|
||||
azureOpenAiApiKey: null,
|
||||
geminiApiKey: null,
|
||||
smtpPassword: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
||||
import { getRuntimeSecretStatuses, resolveSystemSettingsRuntime } from "../lib/system-settings-runtime.js";
|
||||
|
||||
describe("system settings runtime resolution", () => {
|
||||
afterEach(() => {
|
||||
@@ -39,4 +39,27 @@ describe("system settings runtime resolution", () => {
|
||||
|
||||
expect(settings.smtpPassword).toBe("db-password");
|
||||
});
|
||||
|
||||
it("reports active source and legacy DB presence separately", () => {
|
||||
vi.stubEnv("OPENAI_API_KEY", "env-openai-key");
|
||||
|
||||
const statuses = getRuntimeSecretStatuses({
|
||||
aiProvider: "openai",
|
||||
azureOpenAiApiKey: "db-key",
|
||||
smtpPassword: "db-password",
|
||||
});
|
||||
|
||||
expect(statuses.azureOpenAiApiKey).toEqual({
|
||||
configured: true,
|
||||
activeSource: "environment",
|
||||
hasStoredValue: true,
|
||||
envVarNames: ["OPENAI_API_KEY", "AZURE_OPENAI_API_KEY"],
|
||||
});
|
||||
expect(statuses.smtpPassword).toEqual({
|
||||
configured: true,
|
||||
activeSource: "database",
|
||||
hasStoredValue: true,
|
||||
envVarNames: ["SMTP_PASSWORD"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user