feat: implement 2nd gen Cloud Functions declarative security deployment orchestration#10703
feat: implement 2nd gen Cloud Functions declarative security deployment orchestration#10703inlined wants to merge 10 commits into
Conversation
…nt orchestration ### Description This change introduces end-to-end deployment orchestration for declarative security on 2nd generation Cloud Functions. Functions can declare necessary GCP and Firebase IAM roles via the Discovery API. During deployment planning, the CLI verifies IAM permissions, generates a Base38 Salted ETag, and dynamically prompts operators for new role grants or revocations. Deployments follow a zero-downtime rolling security sequence: Phase 0 grants new IAM roles before code upserts, and Phase 2 revokes old roles only after successful traffic migration and function deletes. ### Scenarios Tested - Comprehensive unit tests (`declarativeSecurity.spec.ts`) validating short-circuiting on matching Base38 Salted ETags, verifying opt-out workflows, and interactive role diff prompting. - Unit tests (`iam.spec.ts`, `resourceManager.spec.ts`) verifying literal GCP API helper interactions. - Core deployment planner and fabricator integration specs (`planner.spec.ts`, `fabricator.spec.ts`). ### Sample Commands `npm test`
…planner ### Description This change re-architects declarative security to execute Discovery and IAM role fetching entirely inside `src/deploy/functions/prepare.ts`. Granular properties (`rolesToAdd`, `rolesToRemove`, `serviceAccountToCreate`, `serviceAccountToDelete`) are embedded directly into `CodebasePlan` during `createDeploymentPlan`. Interactive prompting is perfectly unified into `src/deploy/functions/prompts.ts`, and rolling security phases in `Fabricator` are renamed to `grantNewRoles` and `removeOldRoles`. Completely eliminates `declarativeSecurity.ts` and `SecurityPlan` standalone structs. ### Scenarios Tested - 136 passing unit tests across `Fabricator`, `IAM`, and `ResourceManager` suites. - Strictly validated zero double newlines or empty block comment lines across all modified JSDoc and source blocks. ### Sample Commands `npm test`
… security discovery
…rolling from declarative security
…pecification ### Description This change refactors `generateManagedServiceAccountName` to become generic by taking an explicit prefix (`firebase-fn`), ensuring utilities in `/gcp` are decoupled from Functions specifics. Additionally, it refines discoverSecurityDetails error messaging to perfectly match the design document specifications. Also purges all remaining extra newlines (e.g., empty comment block lines and spacing between JSDoc and exports). ### Scenarios Tested - Run full unit and integration test suite (`npm test`) validating zero compile or parser regressions. - Automated and manual audit confirming strict adherence to style guide. ### Sample Commands `npm test`
### Description Reverts the removal of empty lines within JSDoc block comments (` *`), as single empty lines within block comments are permitted by the style guide.
### Description Comprehensively restores all single empty JSDoc comment lines (` *`) that were removed across the entire branch relative to `main`.
### Description Renames the declarative security etag label from firebase-declarative-roles-etag to firebase-declarative-security-etag to match the design notes specification exactly. ### Scenarios Tested - Run unit test suite: npx mocha src/deploy/functions/prepare.spec.ts src/deploy/functions/release/planner.spec.ts src/deploy/functions/release/fabricator.spec.ts - Run linting: npm run lint ### Sample Commands N/A
There was a problem hiding this comment.
Code Review
This pull request introduces declarative security support and automated rolling IAM role grants/revocations for 2nd generation Cloud Functions. It adds discovery, planning, prompting, and fabrication phases to manage codebase-specific service accounts and their associated IAM roles dynamically. Feedback on the changes highlights two critical issues: first, interactive security prompts will cause deployments to hang in non-interactive environments (like CI/CD) unless options.nonInteractive is checked to throw an error; second, deploying with filters while opting out of declarative security can prematurely delete the managed service account, breaking functions not included in the filtered deployment.
| for (const [codebase, codebasePlan] of Object.entries(plan)) { | ||
| if (codebasePlan.serviceAccountToDelete) { |
There was a problem hiding this comment.
In non-interactive environments (such as CI/CD pipelines), attempting to prompt the operator for confirmation will cause the deployment to hang or fail. We should check if 'options.nonInteractive' is true when there are security changes in the plan, and throw a clear 'FirebaseError' instructing the user to use the '--force' flag to bypass the prompts.
for (const [codebase, codebasePlan] of Object.entries(plan)) {
const hasChanges =
!!codebasePlan.serviceAccountToDelete ||
!!codebasePlan.serviceAccountToCreate ||
!!(codebasePlan.rolesToAdd && codebasePlan.rolesToAdd.length > 0) ||
!!(codebasePlan.rolesToRemove && codebasePlan.rolesToRemove.length > 0);
if (hasChanges && options.nonInteractive) {
throw new FirebaseError(
`Declarative security changes for codebase ${codebase} require confirmation. Run with --force to bypass interactive prompts in non-interactive environments.`
);
}
if (codebasePlan.serviceAccountToDelete) {| } else if (existingManagedSA) { | ||
| serviceAccountToDelete = existingManagedSA; | ||
| } |
There was a problem hiding this comment.
When opting out of declarative security ('requiredRoles' is undefined) and deploying with filters (e.g., 'firebase deploy --only functions:func1'), 'serviceAccountToDelete' is set to 'existingManagedSA' and the service account is deleted. This immediately breaks any other functions in the codebase that still rely on the managed service account but were not included in the filtered deployment. We should only delete the service account if no filters are applied (i.e., a full deployment).
| } else if (existingManagedSA) { | |
| serviceAccountToDelete = existingManagedSA; | |
| } | |
| } else if (existingManagedSA && (!filters || filters.length === 0)) { | |
| serviceAccountToDelete = existingManagedSA; | |
| } |
There was a problem hiding this comment.
I don't know if this is necessary but it's worth having. But it can be simplified to !filters?.length
| .toString() | ||
| .padStart(10, "0"); | ||
|
|
||
| const accountId = `${prefix}-${randomSuffix}`; |
| try { | ||
| const fab = new fabricator.Fabricator({ | ||
| functionExecutor, | ||
| // Note: we don't need the temporary concurrency reduction of 2, because that quota limit is for deploys |
| }; | ||
|
|
||
| let resolveCreate: () => void; | ||
|
|
| }; | ||
|
|
||
| const scrapers: scraper.SourceTokenScraper[] = []; | ||
|
|
| `Deleting managed service account ${plan.serviceAccountToDelete}...`, | ||
| ); | ||
| await iam.deleteServiceAccount(this.projectId, plan.serviceAccountToDelete); | ||
| } else if (plan.rolesToRemove && plan.rolesToRemove.length > 0 && plan.managedServiceAccount) { |
There was a problem hiding this comment.
Prefer early exits to keeping the rest of a function nested.
| }; | ||
|
|
||
| const changesets = Object.values(plan); | ||
| for (const [codebase, codebasePlan] of Object.entries(plan)) { |
| * Mutates want Backend to populate managed service account and etag labels. | ||
| * Returns existing discovered roles, or undefined if no security changes needed. | ||
| */ | ||
| export async function discoverSecurityDetails( |
There was a problem hiding this comment.
If possible, break this function up
| for (const [codebase, codebasePlan] of Object.entries(plan)) { | ||
| if (codebasePlan.serviceAccountToDelete) { | ||
| const msg = `Deploying this code will opt out of declarative security for codebase ${codebase}. All functions which do not specify a custom service account will use a default service account on next deploy. As a cleanup, the managed service account ${codebasePlan.serviceAccountToDelete} will be deleted. Continue?`; | ||
| const confirmed = await confirm({ default: false, message: msg }); |
There was a problem hiding this comment.
pass the options object for nonInteractive and force
| throw new FirebaseError("Deployment canceled by user."); | ||
| } | ||
| } else if ( | ||
| (codebasePlan.rolesToAdd && codebasePlan.rolesToAdd.length > 0) || |
There was a problem hiding this comment.
here and elsewhere use nullish coalescing. if (codebasePlan.rolesToAdd?.length || codebasePlan.rolesToRemove?.length)
| ): Promise<string> { | ||
| const maxAttempts = 10; | ||
| for (let i = 0; i < maxAttempts; i++) { | ||
| const randomSuffix = Math.floor(Math.random() * 10000000000) |
There was a problem hiding this comment.
If it is possible, use a better random library. Otherwise leave a note about why this isn't actually a 'secure context'
|
|
||
| for (let i = projectPolicy.bindings.length - 1; i >= 0; i--) { | ||
| const binding = projectPolicy.bindings[i]; | ||
| if (rolesToRemove.includes(binding.role)) { |
There was a problem hiding this comment.
prefer early exits over nesting. E.g. if !includes, continue
…loys ### Description Addresses PR review feedback: 1. Adds options.nonInteractive checks to promptForSecurityChanges to throw a FirebaseError rather than hanging when deploying in non-interactive mode. 2. Updates createDeploymentPlan to prevent deletion of the managed service account when deploying with filters, ensuring that non-deployed functions are not broken when opting out. ### Scenarios Tested - Added unit tests in prompts.spec.ts verifying that promptForSecurityChanges throws FirebaseError in non-interactive mode and prompts successfully in interactive mode. - Added unit tests in planner.spec.ts verifying that the service account is kept during filtered deployments and deleted during unfiltered deployments. ### Sample Commands N/A
Description
This PR implements the deployment orchestration for 2nd generation Cloud Functions for Firebase declarative security (
requiresRole).It extends the CLI wire protocol, validates declared roles, handles the lifecycle of the managed service account (
firebase-fn-<random-suffix>), automates IAM role grants/revocations before and after function deployment/deletion, and prompts operators for confirmations during role modifications.Key changes:
discoverSecurityDetailsinprepare.tsto identify when a codebase uses declarative security and ensure there are no conflicts with manually-defined custom service accounts.firebase-declarative-security-etaglabel on functions to allow non-operators to deploy unchanged codebases without setIamPolicy permissions.grantNewRoles(pre-upsert) andremoveOldRoles(post-delete) phases inFabricatorto create/delete service accounts and grant/revoke roles.promptForSecurityChangesto ask operators for confirmation on rollouts, modifications, or opt-outs.Scenarios Tested
prepare.spec.ts,planner.spec.ts, andfabricator.spec.tsfor all security validation, planning, and rollout states.npm testornpx mocha src/deploy/functions/prepare.spec.ts src/deploy/functions/release/planner.spec.ts src/deploy/functions/release/fabricator.spec.tsnpm run lintSample Commands
firebase deploy --only functions