mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
Fix secrets create provider selection
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0ac5ac5467
commit
e38f34ef69
2 files changed
+186
-30
No files matched your search
@@ -1,8 +1,9 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { SecretProviderDescriptor } from "@paperclipai/shared";
|
||||
import type { CompanySecretProviderConfig, SecretProviderDescriptor } from "@paperclipai/shared";
|
||||
import {
|
||||
findCreateProviderReplacement,
|
||||
getAwsManagedPathPreview,
|
||||
getCreateProviderBlockReason,
|
||||
getDefaultProviderConfigId,
|
||||
@@ -19,6 +20,37 @@ const awsProvider: SecretProviderDescriptor = {
|
||||
configured: true,
|
||||
};
|
||||
|
||||
const localProvider: SecretProviderDescriptor = {
|
||||
id: "local_encrypted",
|
||||
label: "Local encrypted (default)",
|
||||
requiresExternalRef: false,
|
||||
supportsManagedValues: true,
|
||||
supportsExternalReferences: false,
|
||||
configured: true,
|
||||
};
|
||||
|
||||
function providerConfig(
|
||||
overrides: Partial<CompanySecretProviderConfig> & Pick<CompanySecretProviderConfig, "id" | "provider">,
|
||||
): CompanySecretProviderConfig {
|
||||
return {
|
||||
companyId: "company-1",
|
||||
displayName: overrides.id,
|
||||
status: "ready",
|
||||
isDefault: false,
|
||||
config: {},
|
||||
healthStatus: null,
|
||||
healthCheckedAt: null,
|
||||
healthMessage: null,
|
||||
healthDetails: null,
|
||||
disabledAt: null,
|
||||
createdByAgentId: null,
|
||||
createdByUserId: null,
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Secrets page provider helpers", () => {
|
||||
it("previews the derived AWS managed path from provider health details", () => {
|
||||
const health: SecretProviderHealthResponse = {
|
||||
@@ -52,7 +84,9 @@ describe("Secrets page provider helpers", () => {
|
||||
"managed",
|
||||
null,
|
||||
),
|
||||
).toBe("AWS Secrets Manager is not configured in this deployment.");
|
||||
).toBe(
|
||||
"Deployment default AWS Secrets Manager is not configured. Select a ready provider vault or configure the deployment default.",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses provider health copy when an unconfigured provider reports missing bootstrap inputs", () => {
|
||||
@@ -74,27 +108,72 @@ describe("Secrets page provider helpers", () => {
|
||||
health,
|
||||
),
|
||||
).toBe(
|
||||
"AWS Secrets Manager is not configured in this deployment. AWS Secrets Manager provider is not ready: missing PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID.",
|
||||
"Deployment default AWS Secrets Manager is not configured. Select a ready provider vault or configure the deployment default. AWS Secrets Manager provider is not ready: missing PAPERCLIP_SECRETS_AWS_DEPLOYMENT_ID.",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows an unconfigured AWS deployment default when a ready AWS provider vault is selected", () => {
|
||||
expect(
|
||||
getCreateProviderBlockReason(
|
||||
{ ...awsProvider, configured: false },
|
||||
"external",
|
||||
null,
|
||||
providerConfig({
|
||||
id: "aws-prod",
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS prod",
|
||||
status: "ready",
|
||||
}),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("names the selected provider vault block before deployment-default AWS config", () => {
|
||||
expect(
|
||||
getCreateProviderBlockReason(
|
||||
{ ...awsProvider, configured: false },
|
||||
"external",
|
||||
null,
|
||||
providerConfig({
|
||||
id: "aws-disabled",
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS disabled",
|
||||
status: "disabled",
|
||||
}),
|
||||
),
|
||||
).toBe("This provider vault is disabled.");
|
||||
});
|
||||
|
||||
it("blocks provider modes the backend does not support", () => {
|
||||
expect(
|
||||
getCreateProviderBlockReason(
|
||||
{
|
||||
id: "local_encrypted",
|
||||
label: "Local encrypted (default)",
|
||||
requiresExternalRef: false,
|
||||
supportsManagedValues: true,
|
||||
supportsExternalReferences: false,
|
||||
configured: true,
|
||||
},
|
||||
localProvider,
|
||||
"external",
|
||||
null,
|
||||
),
|
||||
).toBe("Local encrypted (default) does not support linked external references.");
|
||||
});
|
||||
|
||||
it("switching to external mode prefers AWS instead of staying on local encrypted when a ready AWS vault exists", () => {
|
||||
expect(
|
||||
findCreateProviderReplacement({
|
||||
providers: [localProvider, { ...awsProvider, configured: false }],
|
||||
providerConfigs: [
|
||||
providerConfig({
|
||||
id: "aws-prod",
|
||||
provider: "aws_secrets_manager",
|
||||
displayName: "AWS prod",
|
||||
status: "ready",
|
||||
isDefault: true,
|
||||
}),
|
||||
],
|
||||
currentProvider: "local_encrypted",
|
||||
mode: "external",
|
||||
health: null,
|
||||
})?.id,
|
||||
).toBe("aws_secrets_manager");
|
||||
});
|
||||
|
||||
it("chooses the ready default provider vault for a provider", () => {
|
||||
expect(
|
||||
getDefaultProviderConfigId(
|
||||
|
||||
+96
-19
@@ -295,6 +295,7 @@ export function getCreateProviderBlockReason(
|
||||
provider: SecretProviderDescriptor | null | undefined,
|
||||
mode: CreateMode,
|
||||
health: SecretProviderHealthResponse | null,
|
||||
providerConfig?: CompanySecretProviderConfig | null,
|
||||
) {
|
||||
if (!provider) return "Select a provider.";
|
||||
if (mode === "managed" && provider.supportsManagedValues === false) {
|
||||
@@ -303,11 +304,20 @@ export function getCreateProviderBlockReason(
|
||||
if (mode === "external" && provider.supportsExternalReferences === false) {
|
||||
return `${provider.label} does not support linked external references.`;
|
||||
}
|
||||
const selectedProviderConfigBlockReason = providerConfig?.provider === provider.id
|
||||
? getProviderConfigBlockReason(providerConfig)
|
||||
: null;
|
||||
const selectedProviderConfigReady =
|
||||
providerConfig?.provider === provider.id && !selectedProviderConfigBlockReason;
|
||||
if (provider.configured === false) {
|
||||
if (selectedProviderConfigReady) return null;
|
||||
if (selectedProviderConfigBlockReason) return selectedProviderConfigBlockReason;
|
||||
const healthEntry = healthEntryForProvider(health, provider.id);
|
||||
const deploymentMessage = `Deployment default ${provider.label} is not configured.`;
|
||||
const nextStep = " Select a ready provider vault or configure the deployment default.";
|
||||
return healthEntry?.message
|
||||
? `${provider.label} is not configured in this deployment. ${healthEntry.message}`
|
||||
: `${provider.label} is not configured in this deployment.`;
|
||||
? `${deploymentMessage}${nextStep} ${healthEntry.message}`
|
||||
: `${deploymentMessage}${nextStep}`;
|
||||
}
|
||||
const healthEntry = healthEntryForProvider(health, provider.id);
|
||||
if (healthEntry?.status === "error") {
|
||||
@@ -319,8 +329,16 @@ export function getCreateProviderBlockReason(
|
||||
function providerHealthText(
|
||||
provider: SecretProviderDescriptor | null | undefined,
|
||||
health: SecretProviderHealthResponse | null,
|
||||
providerConfig?: CompanySecretProviderConfig | null,
|
||||
) {
|
||||
if (!provider) return null;
|
||||
if (
|
||||
provider.configured === false &&
|
||||
providerConfig?.provider === provider.id &&
|
||||
!getProviderConfigBlockReason(providerConfig)
|
||||
) {
|
||||
return `Using selected provider vault. Deployment default ${provider.label} is not configured.`;
|
||||
}
|
||||
const entry = healthEntryForProvider(health, provider.id);
|
||||
if (!entry) return null;
|
||||
const warnings = entry.warnings?.join(" ");
|
||||
@@ -344,20 +362,57 @@ export function getProviderConfigBlockReason(
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getDefaultProviderConfigId(
|
||||
export function getSelectableProviderConfig(
|
||||
configs: CompanySecretProviderConfig[],
|
||||
provider: SecretProvider,
|
||||
) {
|
||||
const providerConfigs = configs.filter((config) => config.provider === provider);
|
||||
const selectable = providerConfigs.filter((config) => !getProviderConfigBlockReason(config));
|
||||
return (
|
||||
selectable.find((config) => config.isDefault)?.id ??
|
||||
selectable[0]?.id ??
|
||||
providerConfigs.find((config) => config.isDefault && !getProviderConfigBlockReason(config)) ??
|
||||
providerConfigs.find((config) => !getProviderConfigBlockReason(config)) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function getDefaultProviderConfigId(
|
||||
configs: CompanySecretProviderConfig[],
|
||||
provider: SecretProvider,
|
||||
) {
|
||||
const selected = getSelectableProviderConfig(configs, provider);
|
||||
const providerConfigs = configs.filter((config) => config.provider === provider);
|
||||
return (
|
||||
selected?.id ??
|
||||
providerConfigs.find((config) => config.isDefault)?.id ??
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
export function findCreateProviderReplacement({
|
||||
providers,
|
||||
providerConfigs,
|
||||
currentProvider,
|
||||
mode,
|
||||
health,
|
||||
}: {
|
||||
providers: SecretProviderDescriptor[];
|
||||
providerConfigs: CompanySecretProviderConfig[];
|
||||
currentProvider: SecretProvider;
|
||||
mode: CreateMode;
|
||||
health: SecretProviderHealthResponse | null;
|
||||
}) {
|
||||
return (
|
||||
providers.find((provider) => {
|
||||
const selectedConfig =
|
||||
provider.id === currentProvider
|
||||
? providerConfigs.find(
|
||||
(config) => config.provider === provider.id && !getProviderConfigBlockReason(config),
|
||||
) ?? null
|
||||
: getSelectableProviderConfig(providerConfigs, provider.id);
|
||||
return !getCreateProviderBlockReason(provider, mode, health, selectedConfig);
|
||||
}) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function providerVaultLabel(configs: CompanySecretProviderConfig[], id: string | null | undefined) {
|
||||
if (!id) return "Deployment default";
|
||||
return configs.find((config) => config.id === id)?.displayName ?? "Unknown vault";
|
||||
@@ -573,11 +628,13 @@ export function Secrets() {
|
||||
selectedCreateProvider,
|
||||
createMode,
|
||||
providerHealthQuery.data ?? null,
|
||||
selectedCreateProviderConfig,
|
||||
) ?? getProviderConfigBlockReason(selectedCreateProviderConfig);
|
||||
const rotateProviderBlockReason = getProviderConfigBlockReason(selectedRotateProviderConfig);
|
||||
const createProviderHealthText = providerHealthText(
|
||||
selectedCreateProvider,
|
||||
providerHealthQuery.data ?? null,
|
||||
selectedCreateProviderConfig,
|
||||
);
|
||||
const awsManagedPathPreview = getAwsManagedPathPreview({
|
||||
provider: selectedCreateProvider,
|
||||
@@ -1006,12 +1063,16 @@ export function Secrets() {
|
||||
providers.find((provider) => provider.id === createForm.provider) ?? null,
|
||||
createMode,
|
||||
providerHealthQuery.data ?? null,
|
||||
providerConfigs.find((config) => config.id === createForm.providerConfigId) ?? null,
|
||||
);
|
||||
if (!currentBlockReason) return;
|
||||
const replacement = providers.find(
|
||||
(provider) =>
|
||||
!getCreateProviderBlockReason(provider, createMode, providerHealthQuery.data ?? null),
|
||||
);
|
||||
const replacement = findCreateProviderReplacement({
|
||||
providers,
|
||||
providerConfigs,
|
||||
currentProvider: createForm.provider,
|
||||
mode: createMode,
|
||||
health: providerHealthQuery.data ?? null,
|
||||
});
|
||||
if (replacement && replacement.id !== createForm.provider) {
|
||||
setCreateForm((current) => ({
|
||||
...current,
|
||||
@@ -1635,7 +1696,7 @@ export function Secrets() {
|
||||
)}
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto p-4 sm:max-w-lg sm:p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingDefinition ? "Edit user-provided secret" : "Create secret"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -1643,7 +1704,7 @@ export function Secrets() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label className="text-xs font-medium" htmlFor="new-secret-name">Name</label>
|
||||
<Input
|
||||
@@ -1721,7 +1782,7 @@ export function Secrets() {
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsList className="grid h-auto w-full grid-cols-2">
|
||||
<TabsTrigger value="company">Company</TabsTrigger>
|
||||
<TabsTrigger value="user">Each user</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -1735,9 +1796,19 @@ export function Secrets() {
|
||||
{secretValueProvider === "company" ? (
|
||||
<>
|
||||
<Tabs value={createMode} onValueChange={(value) => setCreateMode(value as CreateMode)}>
|
||||
<TabsList className="w-full grid grid-cols-2">
|
||||
<TabsTrigger value="managed">Managed value</TabsTrigger>
|
||||
<TabsTrigger value="external">External reference</TabsTrigger>
|
||||
<TabsList className="grid h-auto w-full grid-cols-2">
|
||||
<TabsTrigger
|
||||
value="managed"
|
||||
className="min-h-9 whitespace-normal px-1.5 text-center text-xs leading-tight sm:text-sm"
|
||||
>
|
||||
Managed value
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="external"
|
||||
className="min-h-9 whitespace-normal px-1.5 text-center text-xs leading-tight sm:text-sm"
|
||||
>
|
||||
External reference
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div>
|
||||
@@ -1762,12 +1833,18 @@ export function Secrets() {
|
||||
key={provider.id}
|
||||
value={provider.id}
|
||||
disabled={Boolean(
|
||||
getCreateProviderBlockReason(provider, createMode, providerHealthQuery.data ?? null),
|
||||
getCreateProviderBlockReason(
|
||||
provider,
|
||||
createMode,
|
||||
providerHealthQuery.data ?? null,
|
||||
getSelectableProviderConfig(providerConfigs, provider.id),
|
||||
),
|
||||
)}
|
||||
>
|
||||
{provider.label}
|
||||
{provider.configured === false
|
||||
? " (not configured)"
|
||||
{provider.configured === false &&
|
||||
!getSelectableProviderConfig(providerConfigs, provider.id)
|
||||
? " (deployment default missing)"
|
||||
: provider.requiresExternalRef
|
||||
? " (external only)"
|
||||
: ""}
|
||||
|
||||
Reference in new issue
Block a user