mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
Fail closed on empty GitHub token bindings
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
95a53b52e2
commit
014390c9c0
2 files changed
+75
-7
No files matched your search
@@ -1026,6 +1026,55 @@ describe("resolveExecutionRunAdapterConfig", () => {
|
||||
expect(resolveInjectedEnvBindingForRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["agent", "empty string", { executionRunConfig: { env: { GH_TOKEN: "" } } }],
|
||||
["agent", "empty plain binding", { executionRunConfig: { env: { GITHUB_TOKEN: { type: "plain", value: "" } } } }],
|
||||
["project", "empty string", { projectEnv: { GH_TOKEN: "" } }],
|
||||
["project", "empty plain binding", { projectEnv: { GITHUB_TOKEN: { type: "plain", value: "" } } }],
|
||||
["environment", "empty string", { environmentEnv: { GH_TOKEN: "" } }],
|
||||
["environment", "empty plain binding", { environmentEnv: { GITHUB_TOKEN: { type: "plain", value: "" } } }],
|
||||
["routine", "empty string", { routineEnv: { GH_TOKEN: "" } }],
|
||||
["routine", "empty plain binding", { routineEnv: { GITHUB_TOKEN: { type: "plain", value: "" } } }],
|
||||
])("fails closed for an explicit %s-scoped %s instead of injecting a company secret", async (_scope, _binding, scopeConfig) => {
|
||||
const getByNameInsensitive = vi.fn().mockResolvedValue({ id: "secret-1", name: "GH_TOKEN" });
|
||||
const resolveInjectedEnvBindingForRuntime = vi.fn();
|
||||
|
||||
await expect(resolveExecutionRunAdapterConfig({
|
||||
companyId: "company-1",
|
||||
agentId: "agent-1",
|
||||
issueId: "issue-1",
|
||||
projectId: "project-1",
|
||||
environmentId: "environment-1",
|
||||
routineId: "routine-1",
|
||||
executionRunConfig: { env: {} },
|
||||
projectEnv: null,
|
||||
...scopeConfig,
|
||||
requiredScopedEnvBinding: {
|
||||
keys: ["GH_TOKEN", "GITHUB_TOKEN"],
|
||||
consumerScopes: ["agent", "project", "environment", "routine"],
|
||||
reason: "push_write_credential_missing",
|
||||
remediation: "No GitHub credential found.",
|
||||
fallbackSecretNames: ["GITHUB_TOKEN", "GH_TOKEN", "PAPERCLIP_GITHUB_TOKEN"],
|
||||
fallbackInjectionEnvKey: "GH_TOKEN",
|
||||
},
|
||||
secretsSvc: {
|
||||
resolveAdapterConfigForRuntime: vi.fn(),
|
||||
resolveEnvBindings: vi.fn(),
|
||||
resolveInjectedEnvBindingForRuntime,
|
||||
getByNameInsensitive,
|
||||
} as any,
|
||||
})).rejects.toMatchObject({
|
||||
code: "configuration_incomplete",
|
||||
resultJson: {
|
||||
configurationIncomplete: {
|
||||
reason: "push_write_credential_missing",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(getByNameInsensitive).not.toHaveBeenCalled();
|
||||
expect(resolveInjectedEnvBindingForRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips well-known-secret injection when a supported-scope binding exists", async () => {
|
||||
const getByNameInsensitive = vi.fn();
|
||||
const resolveInjectedEnvBindingForRuntime = vi.fn();
|
||||
|
||||
@@ -824,7 +824,11 @@ function formatMissingBindingForOperator(missing: MissingRuntimeBinding): string
|
||||
return `secret ${secretLabel} not bound at ${missing.consumerType} ${missing.configPath}`;
|
||||
}
|
||||
|
||||
function isConfiguredEnvBindingValue(binding: unknown) {
|
||||
function hasOwnEnvBinding(env: Record<string, unknown> | null, key: string) {
|
||||
return env !== null && Object.prototype.hasOwnProperty.call(env, key);
|
||||
}
|
||||
|
||||
function isUsableEnvBindingValue(binding: unknown) {
|
||||
const parsed = envBindingSchema.safeParse(binding);
|
||||
if (!parsed.success) return false;
|
||||
const value = parsed.data;
|
||||
@@ -1011,23 +1015,38 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
assertLowTrustEnvConfigAllowed(routineEnv, "routine.env");
|
||||
}
|
||||
const requiredScopedEnvBinding = input.requiredScopedEnvBinding ?? null;
|
||||
const requiredScopedBindingPresent = requiredScopedEnvBinding
|
||||
? requiredScopedEnvBinding.keys.some((key) => (
|
||||
requiredScopedEnvBinding.consumerScopes.includes("agent")
|
||||
&& hasOwnEnvBinding(agentEnv, key)
|
||||
) || (
|
||||
requiredScopedEnvBinding.consumerScopes.includes("project")
|
||||
&& hasOwnEnvBinding(projectEnv, key)
|
||||
) || (
|
||||
requiredScopedEnvBinding.consumerScopes.includes("environment")
|
||||
&& hasOwnEnvBinding(environmentEnv, key)
|
||||
) || (
|
||||
requiredScopedEnvBinding.consumerScopes.includes("routine")
|
||||
&& hasOwnEnvBinding(routineEnv, key)
|
||||
))
|
||||
: false;
|
||||
const requiredScopedBindingsConfigured = requiredScopedEnvBinding
|
||||
? requiredScopedEnvBinding.keys.some((key) => (
|
||||
requiredScopedEnvBinding.consumerScopes.includes("agent")
|
||||
&& isConfiguredEnvBindingValue(agentEnv[key])
|
||||
&& isUsableEnvBindingValue(agentEnv[key])
|
||||
) || (
|
||||
requiredScopedEnvBinding.consumerScopes.includes("project")
|
||||
&& isConfiguredEnvBindingValue(projectEnv?.[key])
|
||||
&& isUsableEnvBindingValue(projectEnv?.[key])
|
||||
) || (
|
||||
requiredScopedEnvBinding.consumerScopes.includes("environment")
|
||||
&& isConfiguredEnvBindingValue(environmentEnv?.[key])
|
||||
&& isUsableEnvBindingValue(environmentEnv?.[key])
|
||||
) || (
|
||||
requiredScopedEnvBinding.consumerScopes.includes("routine")
|
||||
&& isConfiguredEnvBindingValue(routineEnv?.[key])
|
||||
&& isUsableEnvBindingValue(routineEnv?.[key])
|
||||
))
|
||||
: false;
|
||||
const ambientHostCredential = requiredScopedEnvBinding
|
||||
&& !requiredScopedBindingsConfigured
|
||||
&& !requiredScopedBindingPresent
|
||||
&& requiredScopedEnvBinding.ambientHostCredential?.enabled
|
||||
? await (
|
||||
requiredScopedEnvBinding.ambientHostCredential.probe ?? probeAmbientHostPushCredential
|
||||
@@ -1042,7 +1061,7 @@ export async function resolveExecutionRunAdapterConfig(input: {
|
||||
} | null = null;
|
||||
if (
|
||||
requiredScopedEnvBinding
|
||||
&& !requiredScopedBindingsConfigured
|
||||
&& !requiredScopedBindingPresent
|
||||
&& !ambientHostCredential
|
||||
&& requiredScopedEnvBinding.fallbackSecretNames?.length
|
||||
&& typeof input.secretsSvc.getByNameInsensitive === "function"
|
||||
|
||||
Reference in new issue
Block a user