mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -239,6 +239,27 @@ export const workflow = pgTable(
|
||||
'workflow',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
/**
|
||||
* Creator and owner. Legitimate as ownership: it anchors personal
|
||||
* (workspace-less) workflows, cascades the workflow away with the account,
|
||||
* and names the owner for webhook config and deploy-as-block resolution.
|
||||
*
|
||||
* @deprecated As an execution identity. Do not use it to decide who a run
|
||||
* acts as, what it may read, or what it may authorize. The acting principal
|
||||
* is `ExecutionMetadata.userId`, which the principal layer
|
||||
* (`resolvePrincipalAttribution`) resolves to the caller for a session,
|
||||
* personal API key, or delegated run, and to the workspace billing account
|
||||
* for a workspace API key, schedule, or webhook.
|
||||
*
|
||||
* Exactly one execution use survives, carried as
|
||||
* `ExecutionMetadata.workflowUserId`: the personal-environment fallback in
|
||||
* `executeWorkflowCore`, for runs with no identifiable caller — workspace
|
||||
* API keys, schedules, webhooks, and unauthenticated public-API calls. Those
|
||||
* have nobody to resolve personal variables as, and a deployed workflow is
|
||||
* routinely authored against its owner's personal keys, so dropping the
|
||||
* fallback would break them. Workspace variables never fall back here; they
|
||||
* always authorize against the actor.
|
||||
*/
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user