diff --git a/cli/src/commands/app/app.ts b/cli/src/commands/app/app.ts index 6b8f74f015..92533ed525 100644 --- a/cli/src/commands/app/app.ts +++ b/cli/src/commands/app/app.ts @@ -18,7 +18,7 @@ import newCommand from "./new.ts"; import generateAgentsCommand from "./generate_agents.ts"; import { isVersionsGeq1585 } from "../sync/global.ts"; import type { PermissionedAsContext } from "../../core/permissioned_as.ts"; -import { resolvePermissionedAsEmail, lookupUsernameByEmail } from "../../core/permissioned_as.ts"; +import { resolvePermissionedAsRule, lookupUsernameByEmail } from "../../core/permissioned_as.ts"; export interface AppFile { value: any; @@ -165,26 +165,28 @@ export async function pushApp( (localApp.policy as any).on_behalf_of = remoteOnBehalfOf; (localApp.policy as any).on_behalf_of_email = remoteOnBehalfOfEmail; preserveFields.preserve_on_behalf_of = true; + log.info(`Preserving ${remoteOnBehalfOfEmail ?? remoteOnBehalfOf} as permissioned_as for app ${remotePath}`); } } else { // Creating: apply defaultPermissionedAs rule if one matches - const ruleEmail = resolvePermissionedAsEmail( + const rule = resolvePermissionedAsRule( remotePath, permissionedAsContext.rules ); - if (ruleEmail) { + if (rule) { // Set both on_behalf_of and on_behalf_of_email on the policy // The backend requires on_behalf_of to be set for preserve to work if (localApp.policy) { const username = await lookupUsernameByEmail( workspace, - ruleEmail, + rule.email, permissionedAsContext.emailToUsernameCache ); (localApp.policy as any).on_behalf_of = `u/${username}`; - (localApp.policy as any).on_behalf_of_email = ruleEmail; + (localApp.policy as any).on_behalf_of_email = rule.email; } preserveFields.preserve_on_behalf_of = true; + log.info(`Setting app ${remotePath} to run permissioned as ${rule.email} (matched rule '${rule.path_pattern}' in wmill.yaml)`); } } } diff --git a/cli/src/commands/flow/flow.ts b/cli/src/commands/flow/flow.ts index 9c5be03631..773decf5fb 100644 --- a/cli/src/commands/flow/flow.ts +++ b/cli/src/commands/flow/flow.ts @@ -21,7 +21,7 @@ import { Flow, OpenFlowWPath } from "../../../gen/types.gen.ts"; import { replaceInlineScripts } from "../../../windmill-utils-internal/src/inline-scripts/replacer.ts"; import { generateFlowLockInternal } from "./flow_metadata.ts"; import type { PermissionedAsContext } from "../../core/permissioned_as.ts"; -import { resolvePermissionedAsEmail } from "../../core/permissioned_as.ts"; +import { resolvePermissionedAsRule } from "../../core/permissioned_as.ts"; export interface FlowFile { summary: string; @@ -76,16 +76,18 @@ export async function pushFlow( if (flow.on_behalf_of_email) { preserveFields.on_behalf_of_email = flow.on_behalf_of_email; preserveFields.preserve_on_behalf_of = true; + log.info(`Preserving ${flow.on_behalf_of_email} as permissioned_as for flow ${remotePath}`); } } else { // Creating: apply defaultPermissionedAs rule if one matches - const ruleEmail = resolvePermissionedAsEmail( + const rule = resolvePermissionedAsRule( remotePath, permissionedAsContext.rules ); - if (ruleEmail) { - preserveFields.on_behalf_of_email = ruleEmail; + if (rule) { + preserveFields.on_behalf_of_email = rule.email; preserveFields.preserve_on_behalf_of = true; + log.info(`Setting flow ${remotePath} to run permissioned as ${rule.email} (matched rule '${rule.path_pattern}' in wmill.yaml)`); } } } diff --git a/cli/src/commands/schedule/schedule.ts b/cli/src/commands/schedule/schedule.ts index 3fbdda79cf..02cc167729 100644 --- a/cli/src/commands/schedule/schedule.ts +++ b/cli/src/commands/schedule/schedule.ts @@ -19,7 +19,7 @@ import { import { Schedule } from "../../../gen/types.gen.ts"; import type { PermissionedAsContext } from "../../core/permissioned_as.ts"; import { - resolvePermissionedAsEmail, + resolvePermissionedAsRule, lookupUsernameByEmail, } from "../../core/permissioned_as.ts"; @@ -126,21 +126,23 @@ export async function pushSchedule( preserveFields.preserve_email = true; if ((schedule as Schedule).edited_by) { preserveFields.email = (schedule as Schedule).edited_by; + log.info(`Preserving ${(schedule as Schedule).edited_by} as permissioned_as for schedule ${path}`); } } else { // Creating: apply defaultPermissionedAs rule if one matches - const ruleEmail = resolvePermissionedAsEmail( + const rule = resolvePermissionedAsRule( path, permissionedAsContext.rules ); - if (ruleEmail) { + if (rule) { const username = await lookupUsernameByEmail( workspace, - ruleEmail, + rule.email, permissionedAsContext.emailToUsernameCache ); preserveFields.email = username; preserveFields.preserve_email = true; + log.info(`Setting schedule ${path} to run permissioned as ${rule.email} (matched rule '${rule.path_pattern}' in wmill.yaml)`); } } } diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 9dbbb04747..ed8d13a278 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -53,7 +53,7 @@ import { createTarBlob, type TarEntry } from "../../utils/tar.ts"; import { execSync } from "node:child_process"; import { NewScript, Script } from "../../../gen/types.gen.ts"; import type { PermissionedAsContext } from "../../core/permissioned_as.ts"; -import { resolvePermissionedAsEmail } from "../../core/permissioned_as.ts"; +import { resolvePermissionedAsRule } from "../../core/permissioned_as.ts"; import { isRawAppBackendPath as isRawAppBackendPathInternal, isAppInlineScriptPath as isAppInlineScriptPathInternal, @@ -433,16 +433,18 @@ export async function handleFile( if (remote.on_behalf_of_email) { requestBodyCommon.on_behalf_of_email = remote.on_behalf_of_email; requestBodyCommon.preserve_on_behalf_of = true; + log.info(`Preserving ${remote.on_behalf_of_email} as permissioned_as for script ${remotePath}`); } } else { // Creating: apply defaultPermissionedAs rule if one matches - const ruleEmail = resolvePermissionedAsEmail( + const rule = resolvePermissionedAsRule( remotePath, permissionedAsContext.rules ); - if (ruleEmail) { - requestBodyCommon.on_behalf_of_email = ruleEmail; + if (rule) { + requestBodyCommon.on_behalf_of_email = rule.email; requestBodyCommon.preserve_on_behalf_of = true; + log.info(`Setting script ${remotePath} to run permissioned as ${rule.email} (matched rule '${rule.path_pattern}' in wmill.yaml)`); } } } diff --git a/cli/src/commands/trigger/trigger.ts b/cli/src/commands/trigger/trigger.ts index 9027a9ee09..1f978b5242 100644 --- a/cli/src/commands/trigger/trigger.ts +++ b/cli/src/commands/trigger/trigger.ts @@ -38,7 +38,7 @@ import { requireLogin } from "../../core/auth.ts"; import { validatePath, resolveWorkspace } from "../../core/context.ts"; import type { PermissionedAsContext } from "../../core/permissioned_as.ts"; import { - resolvePermissionedAsEmail, + resolvePermissionedAsRule, lookupUsernameByEmail, } from "../../core/permissioned_as.ts"; @@ -175,21 +175,23 @@ export async function pushTrigger( preserveFields.preserve_email = true; if ((trigger as any).edited_by) { preserveFields.email = (trigger as any).edited_by; + log.info(`Preserving ${(trigger as any).edited_by} as permissioned_as for trigger ${path}`); } } else { // Creating: apply defaultPermissionedAs rule if one matches - const ruleEmail = resolvePermissionedAsEmail( + const rule = resolvePermissionedAsRule( path, permissionedAsContext.rules ); - if (ruleEmail) { + if (rule) { const username = await lookupUsernameByEmail( workspace, - ruleEmail, + rule.email, permissionedAsContext.emailToUsernameCache ); preserveFields.email = username; preserveFields.preserve_email = true; + log.info(`Setting trigger ${path} to run permissioned as ${rule.email} (matched rule '${rule.path_pattern}' in wmill.yaml)`); } } } diff --git a/cli/src/core/permissioned_as.ts b/cli/src/core/permissioned_as.ts index b775083fd8..cf428afa54 100644 --- a/cli/src/core/permissioned_as.ts +++ b/cli/src/core/permissioned_as.ts @@ -84,16 +84,16 @@ export function validatePermissionedAsRules( } /** - * Resolves which email should be used for a new item based on defaultPermissionedAs rules. - * Returns undefined if no rule matches. + * Resolves which rule should be used for a new item based on defaultPermissionedAs rules. + * Returns the matching rule, or undefined if no rule matches. */ -export function resolvePermissionedAsEmail( +export function resolvePermissionedAsRule( path: string, rules: PermissionedAsRule[] -): string | undefined { +): PermissionedAsRule | undefined { for (const rule of rules) { if (minimatch(path, rule.path_pattern)) { - return rule.email; + return rule; } } return undefined; diff --git a/cli/test/permissioned_as.test.ts b/cli/test/permissioned_as.test.ts index 16509c6931..7a868379cf 100644 --- a/cli/test/permissioned_as.test.ts +++ b/cli/test/permissioned_as.test.ts @@ -5,7 +5,7 @@ import { expect, test, describe } from "bun:test"; import { validatePermissionedAsRules, - resolvePermissionedAsEmail, + resolvePermissionedAsRule, type PermissionedAsRule, } from "../src/core/permissioned_as.ts"; @@ -203,59 +203,58 @@ describe("validatePermissionedAsRules", () => { }); // ============================================================================= -// resolvePermissionedAsEmail +// resolvePermissionedAsRule // ============================================================================= -describe("resolvePermissionedAsEmail", () => { +describe("resolvePermissionedAsRule", () => { const rules: PermissionedAsRule[] = [ { email: "prod@company.com", path_pattern: "f/production/**" }, { email: "staging@company.com", path_pattern: "f/staging/**" }, { email: "default@company.com", path_pattern: "f/**" }, ]; - test("returns matching email for specific path", () => { - expect( - resolvePermissionedAsEmail("f/production/my_script", rules) - ).toBe("prod@company.com"); + test("returns matching rule for specific path", () => { + const rule = resolvePermissionedAsRule("f/production/my_script", rules); + expect(rule?.email).toBe("prod@company.com"); + expect(rule?.path_pattern).toBe("f/production/**"); }); test("returns first matching rule (production over default)", () => { - expect( - resolvePermissionedAsEmail("f/production/deep/nested", rules) - ).toBe("prod@company.com"); + const rule = resolvePermissionedAsRule("f/production/deep/nested", rules); + expect(rule?.email).toBe("prod@company.com"); + expect(rule?.path_pattern).toBe("f/production/**"); }); - test("returns staging email for staging path", () => { - expect( - resolvePermissionedAsEmail("f/staging/my_flow", rules) - ).toBe("staging@company.com"); + test("returns staging rule for staging path", () => { + const rule = resolvePermissionedAsRule("f/staging/my_flow", rules); + expect(rule?.email).toBe("staging@company.com"); + expect(rule?.path_pattern).toBe("f/staging/**"); }); test("falls through to default rule", () => { - expect( - resolvePermissionedAsEmail("f/other/my_script", rules) - ).toBe("default@company.com"); + const rule = resolvePermissionedAsRule("f/other/my_script", rules); + expect(rule?.email).toBe("default@company.com"); + expect(rule?.path_pattern).toBe("f/**"); }); test("returns undefined when no rule matches", () => { expect( - resolvePermissionedAsEmail("u/admin/my_script", rules) + resolvePermissionedAsRule("u/admin/my_script", rules) ).toBeUndefined(); }); test("returns undefined for empty rules", () => { - expect(resolvePermissionedAsEmail("f/anything", [])).toBeUndefined(); + expect(resolvePermissionedAsRule("f/anything", [])).toBeUndefined(); }); test("handles exact path patterns", () => { const exactRules: PermissionedAsRule[] = [ { email: "exact@company.com", path_pattern: "f/specific/script" }, ]; + const rule = resolvePermissionedAsRule("f/specific/script", exactRules); + expect(rule?.email).toBe("exact@company.com"); expect( - resolvePermissionedAsEmail("f/specific/script", exactRules) - ).toBe("exact@company.com"); - expect( - resolvePermissionedAsEmail("f/specific/other", exactRules) + resolvePermissionedAsRule("f/specific/other", exactRules) ).toBeUndefined(); }); @@ -263,11 +262,10 @@ describe("resolvePermissionedAsEmail", () => { const wildcardRules: PermissionedAsRule[] = [ { email: "wild@company.com", path_pattern: "f/*/scripts" }, ]; + const rule = resolvePermissionedAsRule("f/team_a/scripts", wildcardRules); + expect(rule?.email).toBe("wild@company.com"); expect( - resolvePermissionedAsEmail("f/team_a/scripts", wildcardRules) - ).toBe("wild@company.com"); - expect( - resolvePermissionedAsEmail("f/team_a/nested/scripts", wildcardRules) + resolvePermissionedAsRule("f/team_a/nested/scripts", wildcardRules) ).toBeUndefined(); }); });