Skip to content

test(functions): failing repro tests for declarative security managed SA bugs - #10858

Draft
cabljac wants to merge 1 commit into
mainfrom
declarative-security-sa-bug-repro
Draft

test(functions): failing repro tests for declarative security managed SA bugs#10858
cabljac wants to merge 1 commit into
mainfrom
declarative-security-sa-bug-repro

Conversation

@cabljac

@cabljac cabljac commented Jul 28, 2026

Copy link
Copy Markdown

Repro tests for two bugs in the declarative security (requireRoles) managed service account flow, found while migrating storage-resize-images. Draft on purpose: the three new tests fail until the bugs are fixed, and are written against the desired behavior so a fix can land on top of them.

1. Fresh SA propagation race

First deploy with requireRoles creates the managed SA, grants roles, then immediately creates functions. IAM SA creation is eventually consistent, so GCF rejects the brand new SA with 404 ... was not found ... iam.serviceAccounts.actAs. Nothing mitigates this today: grantNewRoles never polls for visibility (src/deploy/functions/release/fabricator.ts:98-118), and the 404 is never retried (DEFAULT_RETRY_CODES = [429, 409, 503] in executor.ts:22; createV2Function's catch only special-cases Cloud Run RESOURCE_EXHAUSTED, fabricator.ts:557-571).

Tests: fabricator.spec.ts "retries function creation when a freshly created service account has not yet propagated" (uses a real QueueExecutor so either a retry-code or fabricator-level fix satisfies it) and "waits for a newly created service account to be visible before returning from grantNewRoles".

2. Failed first deploy orphans the SA, every retry mints a new one

The managed SA is only rediscovered via serviceAccount on already-deployed endpoints (src/deploy/functions/prepare.ts:84-92). If function creation fails after SA creation (bug 1 reproduces this reliably), the next deploy finds no existingManagedSA and unconditionally generates a fresh random firebase-fn-<10digits> name (prepare.ts:141-145, src/gcp/iam.ts:307-330). Each retry leaves another orphaned SA holding project-level role grants (Storage Admin, roles/aiplatform.user in our repro), with no cleanup path.

Test: prepare.spec.ts "reuses an existing managed service account in the project when no deployed functions reference it".

Issues with full repro output: #10859 (propagation race), #10860 (SA orphaning).

…A bugs

Reproduces two bugs in the requireRoles managed service account flow:

- createV2Function does not retry the 404 GCF returns when a freshly
  created service account has not propagated through IAM, and
  grantNewRoles never polls for SA visibility before function creation
- discoverSecurityDetails only rediscovers the managed SA via deployed
  endpoints, so a failed first deploy orphans the SA (and its
  project-level role grants) and every retry creates a new one

Tests intentionally fail until the bugs are fixed.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds several unit tests to reproduce and document existing bugs related to service account creation, propagation, and reuse during deployment. The reviewer feedback focuses on improving the robustness of these tests by stubbing iam.getServiceAccount in beforeEach blocks to prevent other tests from failing once the fixes are implemented. Additionally, the reviewer recommends avoiding as any type assertions to adhere to the repository's TypeScript style guide, suggesting type intersections or fully-formed mock objects instead.

Comment on lines +1209 to +1211
generateManagedSANameStub = sinon
.stub(iam, "generateManagedServiceAccountName")
.resolves("firebase-fn-123");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the fix for reusing existing managed service accounts is implemented, discoverSecurityDetails will likely call iam.getServiceAccount to check if the SA already exists in the project. Since iam.getServiceAccount is not stubbed in the beforeEach block of describe("discoverSecurityDetails"), all other tests in this suite that call discoverSecurityDetails will attempt to make real API calls and fail.\n\nTo prevent this, stub iam.getServiceAccount in the beforeEach block to reject with a 404 error by default, representing the happy path where the SA does not yet exist.

      generateManagedSANameStub = sinon\n        .stub(iam, "generateManagedServiceAccountName")\n        .resolves("firebase-fn-123");\n      sinon.stub(iam, "getServiceAccount").rejects({ status: 404 });

Comment on lines +676 to +679
const saNotFoundErr = new Error(
"Service account projects/-/serviceAccounts/firebase-fn-123@test-project.iam.gserviceaccount.com was not found. Please verify that the caller has iam.serviceAccounts.actAs permission on the service account.",
);
(saNotFoundErr as any).status = 404;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using as any as an escape hatch to set the status property on the error object. This violates the repository style guide standard regarding TypeScript types.\n\nInstead, use a type intersection Error & { status?: number } to safely and cleanly add the status property.

      const saNotFoundErr = new Error(\n        "Service account projects/-/serviceAccounts/firebase-fn-123@test-project.iam.gserviceaccount.com was not found. Please verify that the caller has iam.serviceAccounts.actAs permission on the service account."\n      ) as Error & { status?: number };\n      saNotFoundErr.status = 404;
References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment on lines +2103 to +2106
const getServiceAccountStub = sinon.stub(iam, "getServiceAccount").resolves({
name: "projects/test-project/serviceAccounts/firebase-fn-123@my-proj.iam.gserviceaccount.com",
email: "firebase-fn-123@my-proj.iam.gserviceaccount.com",
} as any);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using as any to cast the resolved value of getServiceAccount. This violates the repository style guide standard regarding TypeScript types. Instead, provide a fully-formed ServiceAccount mock object.\n\nAdditionally, since grantNewRoles will be modified to call iam.getServiceAccount to poll for propagation, other tests in this describe block (such as "should create SA and grant roles in grantNewRoles") will also trigger this call. To prevent them from attempting real API calls and failing once the fix is implemented, consider stubbing iam.getServiceAccount in the beforeEach block of describe("declarative security phases") instead of only inside this specific test.

      const getServiceAccountStub = sinon.stub(iam, "getServiceAccount").resolves({\n        name: "projects/test-project/serviceAccounts/firebase-fn-123@my-proj.iam.gserviceaccount.com",\n        projectId: "test-project",\n        uniqueId: "123",\n        email: "firebase-fn-123@my-proj.iam.gserviceaccount.com",\n        displayName: "",\n        etag: "",\n        description: "",\n        oauth2ClientId: "",\n        disabled: false,\n      });
References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant