From 9a9114ea48c0bf36e45c814bdc5a72d949ca7251 Mon Sep 17 00:00:00 2001 From: EagleFox31 <86088743+EagleFox31@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:18:17 +0100 Subject: [PATCH 1/3] fix(oidc): trust only exact Mukuru pilot workflow on main for staging DB lease --- src/auth.ts | 64 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index daa1955..1311906 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -298,40 +298,64 @@ export async function authenticateInfrastructureMutation( * only explicitly approved collector workflows on protected main may request * their own database lease. Cross-repository credential selection is forbidden. */ -export async function authenticateDatabaseLease( - request: Request, - env: Env -): Promise { - const { claims, expectedRef } = await verifyOidcToken(request, env); - const approvedWorkflows: Record = { - "Trigenys/trigenys-seo-monitor": - `Trigenys/trigenys-seo-monitor/.github/workflows/seo-monitor.yml@${expectedRef}`, - "Trigenys/trigenys-editorial-os": - `Trigenys/trigenys-editorial-os/.github/workflows/news-scout.yml@${expectedRef}` +interface ApprovedDatabaseLeaseCaller { + workflow: string; + events: readonly string[]; +} + +/** + * An OIDC identity is scoped to an exact workflow file, main ref and event. + * Mukuru is a bounded staging editorial pilot; it cannot select a DB profile. + * This pure authorizer is tested against both allowed and denied identities. + */ +export function authorizeDatabaseLeaseClaims( + claims: GitHubOidcClaims, + expectedRef: string +): void { + const approvedWorkflows: Record = { + "Trigenys/trigenys-seo-monitor": [ + { + workflow: `Trigenys/trigenys-seo-monitor/.github/workflows/seo-monitor.yml@${expectedRef}`, + events: ["schedule", "workflow_dispatch"] + } + ], + "Trigenys/trigenys-editorial-os": [ + { + workflow: `Trigenys/trigenys-editorial-os/.github/workflows/news-scout.yml@${expectedRef}`, + events: ["schedule", "workflow_dispatch", "push"] + }, + { + workflow: `Trigenys/trigenys-editorial-os/.github/workflows/mukuru-editorial-pilot.yml@${expectedRef}`, + events: ["push", "workflow_dispatch"] + } + ] }; const repository = claims.repository || ""; - const allowedWorkflow = approvedWorkflows[repository]; - if (!allowedWorkflow || claims.workflow_ref !== allowedWorkflow) { + const allowedWorkflow = (approvedWorkflows[repository] || []).find( + (item) => item.workflow === claims.workflow_ref + ); + if (!allowedWorkflow) { throw new AuthenticationError( 403, "DATABASE_LEASE_CALLER_FORBIDDEN", "Database access is restricted to approved collector workflows on main." ); } - const eventAllowed = - claims.event_name === "schedule" || - claims.event_name === "workflow_dispatch" || - ( - repository === "Trigenys/trigenys-editorial-os" && - claims.event_name === "push" - ); - if (!eventAllowed) { + if (!claims.event_name || !allowedWorkflow.events.includes(claims.event_name)) { throw new AuthenticationError( 403, "DATABASE_LEASE_EVENT_FORBIDDEN", "Database lease event is not allowed for this collector." ); } +} + +export async function authenticateDatabaseLease( + request: Request, + env: Env +): Promise { + const { claims, expectedRef } = await verifyOidcToken(request, env); + authorizeDatabaseLeaseClaims(claims, expectedRef); return claims; } From 91b68195412bbbe6d43240f9b8837eaf146ee83d Mon Sep 17 00:00:00 2001 From: EagleFox31 <86088743+EagleFox31@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:18:31 +0100 Subject: [PATCH 2/3] test(oidc): exact Mukuru workflow event/ref allowlist and negative cases --- test/database-lease.test.mjs | 49 ++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/test/database-lease.test.mjs b/test/database-lease.test.mjs index 8b22416..6f9546e 100644 --- a/test/database-lease.test.mjs +++ b/test/database-lease.test.mjs @@ -9,6 +9,8 @@ const leaseSource=fs.readFileSync("src/database-lease.ts","utf8"); const types=fs.readFileSync("src/types.ts","utf8"); const js=ts.transpileModule(leaseSource,{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText; const {issueDatabaseLease}=await import("data:text/javascript;base64,"+Buffer.from(js).toString("base64")); +const authJs=ts.transpileModule(auth,{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText; +const {authorizeDatabaseLeaseClaims}=await import("data:text/javascript;base64,"+Buffer.from(authJs).toString("base64")); const seoClaims={repository:"Trigenys/trigenys-seo-monitor"}; const seoUrl="postgresql://seo_monitor_owner:test-not-a-real-password@ep-demo.c-6.eu-central-1.aws.neon.tech:5432/seo_monitor?sslmode=require"; @@ -27,13 +29,56 @@ test("OIDC database lease trust is pinned to approved collector workflows on mai assert.match(auth,/\.github\/workflows\/seo-monitor\.yml@/); assert.match(auth,/Trigenys\/trigenys-editorial-os/); assert.match(auth,/\.github\/workflows\/news-scout\.yml@/); - assert.match(auth,/claims\.workflow_ref !== allowedWorkflow/); + assert.match(auth,/\.github\/workflows\/mukuru-editorial-pilot\.yml@/); + assert.match(auth,/authorizeDatabaseLeaseClaims/); + assert.match(auth,/item\.workflow === claims\.workflow_ref/); assert.match(auth,/claims\.event_name === "schedule"/); assert.match(auth,/claims\.event_name === "workflow_dispatch"/); - assert.match(auth,/repository === "Trigenys\/trigenys-editorial-os"/); + assert.match(auth,/events: \["push", "workflow_dispatch"\]/); assert.match(auth,/claims\.event_name === "push"/); }); +test("Mukuru staging OIDC identity accepts only its canonical workflow on main",()=>{ + const ref="refs/heads/main"; + const news="Trigenys/trigenys-editorial-os/.github/workflows/news-scout.yml@"+ref; + const pilot="Trigenys/trigenys-editorial-os/.github/workflows/mukuru-editorial-pilot.yml@"+ref; + const seo="Trigenys/trigenys-seo-monitor/.github/workflows/seo-monitor.yml@"+ref; + const base={iss:"fixture",aud:"appfactory-api",exp:9999999999,repository:"Trigenys/trigenys-editorial-os",ref}; + for(const event_name of ["push","workflow_dispatch"]){ + assert.doesNotThrow(()=>authorizeDatabaseLeaseClaims({...base,workflow_ref:pilot,event_name},ref)); + } + for(const event_name of ["push","schedule","workflow_dispatch"]){ + assert.doesNotThrow(()=>authorizeDatabaseLeaseClaims({...base,workflow_ref:news,event_name},ref)); + } + assert.throws( + ()=>authorizeDatabaseLeaseClaims({...base,workflow_ref:pilot,event_name:"schedule"},ref), + error=>error.code==="DATABASE_LEASE_EVENT_FORBIDDEN" + ); + for(const workflow_ref of [ + "Trigenys/trigenys-editorial-os/.github/workflows/random.yml@"+ref, + pilot.replace("@refs/heads/main","@refs/heads/dev"), + "Trigenys/other/.github/workflows/mukuru-editorial-pilot.yml@"+ref + ]){ + assert.throws( + ()=>authorizeDatabaseLeaseClaims({...base,workflow_ref,event_name:"push"},ref), + error=>error.code==="DATABASE_LEASE_CALLER_FORBIDDEN" + ); + } + assert.throws( + ()=>authorizeDatabaseLeaseClaims({...base,repository:"Trigenys/other",workflow_ref:pilot,event_name:"push"},ref), + error=>error.code==="DATABASE_LEASE_CALLER_FORBIDDEN" + ); + assert.throws( + ()=>authorizeDatabaseLeaseClaims({...base,workflow_ref:seo,event_name:"push"},ref), + error=>error.code==="DATABASE_LEASE_CALLER_FORBIDDEN" + ); + assert.doesNotThrow(()=>authorizeDatabaseLeaseClaims({...base,repository:"Trigenys/trigenys-seo-monitor",workflow_ref:seo,event_name:"schedule"},ref)); + assert.throws( + ()=>authorizeDatabaseLeaseClaims({...base,repository:"Trigenys/trigenys-seo-monitor",workflow_ref:seo,event_name:"push"},ref), + error=>error.code==="DATABASE_LEASE_EVENT_FORBIDDEN" + ); +}); + test("only exact repository requests allowed and arbitrary profile selectors rejected",()=>{ assert.throws(()=>issueDatabaseLease(seoEnv,{repository:"Trigenys/other"},seoRequest),e=>e.code==="DATABASE_LEASE_REPOSITORY_FORBIDDEN"); assert.throws(()=>issueDatabaseLease(seoEnv,seoClaims,{repository:"Trigenys/other"}),e=>e.code==="DATABASE_LEASE_REPOSITORY_MISMATCH"); From cf7ebf0e81f6c4071fefe37deb0319c53ee891be Mon Sep 17 00:00:00 2001 From: EagleFox31 <86088743+EagleFox31@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:19:34 +0100 Subject: [PATCH 3/3] test(oidc): replace obsolete source-regex checks with executable event authorization tests --- test/database-lease.test.mjs | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/database-lease.test.mjs b/test/database-lease.test.mjs index 6f9546e..023aa93 100644 --- a/test/database-lease.test.mjs +++ b/test/database-lease.test.mjs @@ -32,10 +32,7 @@ test("OIDC database lease trust is pinned to approved collector workflows on mai assert.match(auth,/\.github\/workflows\/mukuru-editorial-pilot\.yml@/); assert.match(auth,/authorizeDatabaseLeaseClaims/); assert.match(auth,/item\.workflow === claims\.workflow_ref/); - assert.match(auth,/claims\.event_name === "schedule"/); - assert.match(auth,/claims\.event_name === "workflow_dispatch"/); assert.match(auth,/events: \["push", "workflow_dispatch"\]/); - assert.match(auth,/claims\.event_name === "push"/); }); test("Mukuru staging OIDC identity accepts only its canonical workflow on main",()=>{