Skip to content

Commit e44a811

Browse files
committed
Prompt required IAM permission during frameworks onboarding.
1 parent 505ca18 commit e44a811

File tree

3 files changed

+51
-7
lines changed

3 files changed

+51
-7
lines changed

src/gcp/cloudbuild.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,10 @@ export async function deleteRepository(
209209
const res = await client.delete<Operation>(name);
210210
return res.body;
211211
}
212+
213+
/**
214+
* Returns email associated with the Cloud Build Service Agent.
215+
*/
216+
export function serviceAgentEmail(projectNumber: string): string {
217+
return `service-${projectNumber}@gcp-sa-cloudbuild.iam.gserviceaccount.com`;
218+
}

src/init/features/frameworks/repo.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import * as clc from "colorette";
22

33
import * as gcb from "../../../gcp/cloudbuild";
4+
import * as rm from "../../../gcp/resourceManager";
45
import * as poller from "../../../operation-poller";
56
import * as utils from "../../../utils";
67
import { cloudbuildOrigin } from "../../../api";
78
import { FirebaseError } from "../../../error";
89
import { logger } from "../../../logger";
910
import { promptOnce } from "../../../prompt";
11+
import { getProjectNumber } from "../../../getProjectNumber";
1012

1113
export interface ConnectionNameParts {
1214
projectId: string;
@@ -81,6 +83,10 @@ export async function linkGitHubRepository(
8183
logger.info(clc.bold(`\n${clc.yellow("===")} Connect a GitHub repository`));
8284
const existingConns = await listFrameworksConnections(projectId);
8385
if (existingConns.length < 1) {
86+
const grantSuccess = await promptSecretManagerAdminGrant(projectId);
87+
if (!grantSuccess) {
88+
throw new FirebaseError("Insufficient IAM permissions to create a new connection to GitHub");
89+
}
8490
let oauthConn = await getOrCreateConnection(projectId, location, FRAMEWORKS_OAUTH_CONN_NAME);
8591
while (oauthConn.installationState.stage === "PENDING_USER_OAUTH") {
8692
oauthConn = await promptConnectionAuth(oauthConn);
@@ -156,14 +162,40 @@ async function promptRepositoryUri(
156162
return { remoteUri, connection: remoteUriToConnection[remoteUri] };
157163
}
158164

165+
async function promptSecretManagerAdminGrant(projectId: string): Promise<Boolean> {
166+
const projectNumber = await getProjectNumber({ projectId });
167+
const cbsaEmail = gcb.serviceAgentEmail(projectNumber);
168+
logger.info(
169+
"To create a new GitHub connection, Secret Manager Admin role (roles/secretmanager.admin) is required on the Cloud Build Service Agent."
170+
);
171+
const grant = await promptOnce({
172+
type: "confirm",
173+
message: "Grant the required role to the Cloud Build Service Agent?",
174+
});
175+
if (!grant) {
176+
logger.info(
177+
"You, or your project administrator, should run the following command to grant the required role:\n\n" +
178+
`\tgcloud projects add-iam-policy-binding ${projectId} \\\n` +
179+
`\t --member="serviceAccount:${cbsaEmail} \\\n` +
180+
`\t --role="roles/secretmanager.admin\n`
181+
);
182+
return false;
183+
}
184+
await rm.addServiceAccountToRoles(projectId, cbsaEmail, ["roles/secretmanager.admin"], true);
185+
logger.info("Successfully granted the required role to the Cloud Build Service Agent!");
186+
return true;
187+
}
188+
159189
async function promptConnectionAuth(conn: gcb.Connection): Promise<gcb.Connection> {
160190
logger.info("You must authorize the Cloud Build GitHub app.");
161191
logger.info();
162-
logger.info("First, sign in to GitHub and authorize Cloud Build GitHub app:");
163-
const cleanup = await utils.openInBrowserPopup(
192+
logger.info("Sign in to GitHub and authorize Cloud Build GitHub app:");
193+
const { url, cleanup } = await utils.openInBrowserPopup(
164194
conn.installationState.actionUri,
165195
"Authorize the GitHub app"
166196
);
197+
logger.info(`\t${url}`);
198+
logger.info();
167199
await promptOnce({
168200
type: "input",
169201
message: "Press Enter once you have authorized the app",
@@ -215,7 +247,7 @@ export async function getOrCreateConnection(
215247
try {
216248
conn = await gcb.getConnection(projectId, location, connectionId);
217249
} catch (err: unknown) {
218-
if ((err as FirebaseError).status === 404) {
250+
if ((err as any).status === 404) {
219251
conn = await createConnection(projectId, location, connectionId, githubConfig);
220252
} else {
221253
throw err;

src/utils.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,10 @@ export async function openInBrowser(url: string): Promise<void> {
767767
/**
768768
* Like openInBrowser but opens the url in a popup.
769769
*/
770-
export async function openInBrowserPopup(url: string, buttonText: string): Promise<() => void> {
770+
export async function openInBrowserPopup(
771+
url: string,
772+
buttonText: string
773+
): Promise<{ url: string; cleanup: () => void }> {
771774
const popupPage = fs
772775
.readFileSync(path.join(__dirname, "../templates/popup.html"), { encoding: "utf-8" })
773776
.replace("${url}", url)
@@ -787,10 +790,12 @@ export async function openInBrowserPopup(url: string, buttonText: string): Promi
787790
server.listen(port);
788791

789792
const popupPageUri = `http://localhost:${port}`;
790-
logger.info(popupPageUri);
791793
await openInBrowser(popupPageUri);
792794

793-
return () => {
794-
server.close();
795+
return {
796+
url: popupPageUri,
797+
cleanup: () => {
798+
server.close();
799+
},
795800
};
796801
}

0 commit comments

Comments
 (0)