fix(execution): resolve secrets against the acting principal, not the workflow owner (#6690)

* fix(execution): resolve secrets against the acting principal, not the workflow owner

* fix(execution): resolve anonymous public-API runs as the workspace billing account

* fix(execution): propagate run identity across dispatch paths and scope public runs to workspace secrets
This commit is contained in:
Vikhyath Mondreti
2026-08-13 18:50:15 -07:00
committed by GitHub
parent 009f5fea7f
commit fa394e5e07
16 changed files with 488 additions and 20 deletions
@@ -30,12 +30,63 @@ function emptyPersonalAndWorkspaceEnv(): {
* environmentUtilsMockFns.mockGetEffectiveDecryptedEnv.mockResolvedValue({ API_KEY: 'k' })
* ```
*/
/**
* Mirrors the real resolver: one lookup when both identities match, two when the
* execution actor differs from the identity owning the personal variables.
* Delegating keeps `mockGetPersonalAndWorkspaceEnv` the single place a test has
* to stub environment data.
*/
async function delegateExecutionEnvironment(
personalUserId: string | undefined,
workspaceUserId: string,
workspaceId?: string
) {
const resolve = environmentUtilsMockFns.mockGetPersonalAndWorkspaceEnv
if (personalUserId === undefined) {
const workspaceOnly = await resolve(workspaceUserId, workspaceId)
return {
...workspaceOnly,
personalEncrypted: {},
personalDecrypted: {},
personalOwners: {},
conflicts: [],
decryptionFailures: (workspaceOnly.decryptionFailures ?? []).filter(
(k: string) => k in (workspaceOnly.workspaceEncrypted ?? {})
),
}
}
if (!workspaceId || workspaceUserId === personalUserId) {
return resolve(personalUserId, workspaceId)
}
const [personal, actor] = await Promise.all([
resolve(personalUserId, workspaceId),
resolve(workspaceUserId, workspaceId),
])
const personalEncrypted = personal.personalEncrypted ?? {}
const workspaceEncrypted = actor.workspaceEncrypted ?? {}
return {
...personal,
workspaceEncrypted: actor.workspaceEncrypted,
workspaceDecrypted: actor.workspaceDecrypted,
conflicts: Object.keys(personalEncrypted).filter((key) => key in workspaceEncrypted),
decryptionFailures: [
...new Set([
...(personal.decryptionFailures ?? []).filter((k: string) => k in personalEncrypted),
...(actor.decryptionFailures ?? []).filter((k: string) => k in workspaceEncrypted),
]),
],
}
}
export const environmentUtilsMockFns = {
mockInvalidateEffectiveDecryptedEnvCache: vi.fn(),
mockGetEnvironmentVariableKeys: vi.fn().mockResolvedValue({ variableNames: [], count: 0 }),
mockGetPersonalAndWorkspaceEnv: vi
.fn()
.mockImplementation(async () => emptyPersonalAndWorkspaceEnv()),
mockGetExecutionEnvironment: vi.fn().mockImplementation(delegateExecutionEnvironment),
mockGetEffectiveEnvironmentSnapshot: vi
.fn()
.mockImplementation(async () => emptyPersonalAndWorkspaceEnv()),
@@ -55,6 +106,9 @@ export function resetEnvironmentUtilsMock(): void {
environmentUtilsMockFns.mockGetPersonalAndWorkspaceEnv
.mockReset()
.mockImplementation(async () => emptyPersonalAndWorkspaceEnv())
environmentUtilsMockFns.mockGetExecutionEnvironment
.mockReset()
.mockImplementation(delegateExecutionEnvironment)
environmentUtilsMockFns.mockGetEffectiveEnvironmentSnapshot
.mockReset()
.mockImplementation(async () => emptyPersonalAndWorkspaceEnv())
@@ -79,6 +133,7 @@ export const environmentUtilsMock = {
environmentUtilsMockFns.mockInvalidateEffectiveDecryptedEnvCache,
getEnvironmentVariableKeys: environmentUtilsMockFns.mockGetEnvironmentVariableKeys,
getPersonalAndWorkspaceEnv: environmentUtilsMockFns.mockGetPersonalAndWorkspaceEnv,
getExecutionEnvironment: environmentUtilsMockFns.mockGetExecutionEnvironment,
getEffectiveEnvironmentSnapshot: environmentUtilsMockFns.mockGetEffectiveEnvironmentSnapshot,
upsertPersonalEnvVars: environmentUtilsMockFns.mockUpsertPersonalEnvVars,
upsertWorkspaceEnvVars: environmentUtilsMockFns.mockUpsertWorkspaceEnvVars,