feat: add IAM and Resource Manager utilities for service accounts (PR 2)#10733
feat: add IAM and Resource Manager utilities for service accounts (PR 2)#10733inlined wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces several IAM utility functions, including getRoleName, generateManagedServiceAccountName, computeRolesEtag, removeServiceAccountRoles, and getServiceAccountRoles, along with a local map of known roles and corresponding unit tests. The review feedback focuses on improving type safety and robustness: specifically, avoiding the use of any by utilizing the getErrStatus utility, using a default import for the JSON roles map, adhering to strict null checks for role lookups, and defensively handling potentially undefined bindings in the project policy to prevent runtime crashes.
| } catch (err: any) { | ||
| if (err.status === 404) { | ||
| return accountId; | ||
| } | ||
| throw err; | ||
| } |
There was a problem hiding this comment.
Avoid using any as an escape hatch. Instead, use the getErrStatus utility from ../error to safely check the error status in a type-safe manner.
| } catch (err: any) { | |
| if (err.status === 404) { | |
| return accountId; | |
| } | |
| throw err; | |
| } | |
| } catch (err) { | |
| if (getErrStatus(err) === 404) { | |
| return accountId; | |
| } | |
| throw err; | |
| } |
References
- Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)
| import { logger } from "../logger"; | ||
| import { Client } from "../apiv2"; | ||
| import * as utils from "../utils"; | ||
| import { FirebaseError } from "../error"; |
| import { Client } from "../apiv2"; | ||
| import * as utils from "../utils"; | ||
| import { FirebaseError } from "../error"; | ||
| import * as knownRoles from "./knownRoles.json"; |
There was a problem hiding this comment.
Use a default import for the JSON file instead of a namespace import. When using import * as knownRoles, the imported object is a module namespace containing a default property (under esModuleInterop), which causes type mismatch issues when assigning it to Record<string, string>.
| import * as knownRoles from "./knownRoles.json"; | |
| import knownRoles from "./knownRoles.json"; |
| * Short-circuits using a local map of known roles for speed, and falls back to the GCP API. | ||
| */ | ||
| export async function getRoleName(role: string): Promise<string> { | ||
| const map: Record<string, string> = knownRoles; |
There was a problem hiding this comment.
Type the map as Record<string, string | undefined> to adhere to strict null checks. Since not all arbitrary strings are valid keys in the knownRoles map, accessing map[role] can return undefined.
| const map: Record<string, string> = knownRoles; | |
| const map: Record<string, string | undefined> = knownRoles; |
References
- Use strict null checks and handle undefined/null explicitly. (link)
| const projectPolicy = await getIamPolicy(projectId); | ||
| const memberName = `serviceAccount:${serviceAccountEmail}`; |
There was a problem hiding this comment.
Defensively initialize projectPolicy.bindings to an empty array if it is undefined. GCP's getIamPolicy API can return a policy without the bindings field if no bindings exist, which would cause a runtime crash (TypeError: Cannot read properties of undefined (reading 'length')) when attempting to iterate over it.
| const projectPolicy = await getIamPolicy(projectId); | |
| const memberName = `serviceAccount:${serviceAccountEmail}`; | |
| const projectPolicy = await getIamPolicy(projectId); | |
| projectPolicy.bindings = projectPolicy.bindings || []; | |
| const memberName = `serviceAccount:${serviceAccountEmail}`; |
| for (const binding of projectPolicy.bindings) { | ||
| if (binding.members.includes(memberName)) { | ||
| roles.push(binding.role); | ||
| } | ||
| } |
There was a problem hiding this comment.
Defensively handle the case where projectPolicy.bindings is undefined to prevent a potential runtime crash when iterating over the bindings.
| for (const binding of projectPolicy.bindings) { | |
| if (binding.members.includes(memberName)) { | |
| roles.push(binding.role); | |
| } | |
| } | |
| for (const binding of projectPolicy.bindings || []) { | |
| if (binding.members.includes(memberName)) { | |
| roles.push(binding.role); | |
| } | |
| } |
edc4799 to
e0c76d9
Compare
4a06f05 to
1ddf608
Compare
e0c76d9 to
771a36b
Compare
### Description This is PR 2 in the requiresRole PR chain. It implements low-level Cloud IAM and Resource Manager API utilities: 1. generateManagedServiceAccountName for collision-resolved SA names. 2. computeRolesEtag for salt-and-hash role etag calculations. 3. addServiceAccountRoles and removeServiceAccountRoles to mutate project IAM policies. 4. Static cache of standard roles in knownRoles.json to prevent API roundtrips. ### Scenarios Tested - Run unit tests: npx mocha src/gcp/iam.spec.ts src/gcp/resourceManager.spec.ts - Run npm run build & npm run lint ### Sample Commands N/A
1ddf608 to
b4dd6a2
Compare
Description
This is PR 2 in the requiresRole PR chain.
It implements low-level Cloud IAM and Resource Manager API utilities:
generateManagedServiceAccountNamefor collision-resolved SA names.computeRolesEtagfor salt-and-hash role etag calculations.addServiceAccountRolesandremoveServiceAccountRolesto mutate project IAM policies.knownRoles.jsonto prevent API roundtrips.Scenarios Tested
npx mocha src/gcp/iam.spec.ts src/gcp/resourceManager.spec.tsSample Commands
N/A