Skip to content

Commit

Permalink
Prompt required IAM permission during frameworks onboarding.
Browse files Browse the repository at this point in the history
  • Loading branch information
taeold committed Dec 6, 2023
1 parent 8637922 commit 1a38ffb
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 7 deletions.
7 changes: 7 additions & 0 deletions src/gcp/cloudbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,10 @@ export async function deleteRepository(
const res = await client.delete<Operation>(name);
return res.body;
}

/**
* Returns email associated with the Cloud Build Service Agent.
*/
export function serviceAgentEmail(projectNumber: string): string {
return `service-${projectNumber}@gcp-sa-cloudbuild.iam.gserviceaccount.com`;
}
38 changes: 35 additions & 3 deletions src/init/features/frameworks/repo.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as clc from "colorette";

import * as gcb from "../../../gcp/cloudbuild";
import * as rm from "../../../gcp/resourceManager";
import * as poller from "../../../operation-poller";
import * as utils from "../../../utils";
import { cloudbuildOrigin } from "../../../api";
import { FirebaseError } from "../../../error";
import { logger } from "../../../logger";
import { promptOnce } from "../../../prompt";
import { getProjectNumber } from "../../../getProjectNumber";

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

async function promptSecretManagerAdminGrant(projectId: string): Promise<Boolean> {
const projectNumber = await getProjectNumber({ projectId });
const cbsaEmail = gcb.serviceAgentEmail(projectNumber);
logger.info(
"To create a new GitHub connection, Secret Manager Admin role (roles/secretmanager.admin) is required on the Cloud Build Service Agent."
);
const grant = await promptOnce({
type: "confirm",
message: "Grant the required role to the Cloud Build Service Agent?",
});
if (!grant) {
logger.info(
"You, or your project administrator, should run the following command to grant the required role:\n\n" +
`\tgcloud projects add-iam-policy-binding ${projectId} \\\n` +
`\t --member="serviceAccount:${cbsaEmail} \\\n` +
`\t --role="roles/secretmanager.admin\n`
);
return false;
}
await rm.addServiceAccountToRoles(projectId, cbsaEmail, ["roles/secretmanager.admin"], true);
logger.info("Successfully granted the required role to the Cloud Build Service Agent!");
return true;
}

async function promptConnectionAuth(conn: gcb.Connection): Promise<gcb.Connection> {
logger.info("You must authorize the Cloud Build GitHub app.");
logger.info();
logger.info("First, sign in to GitHub and authorize Cloud Build GitHub app:");
const cleanup = await utils.openInBrowserPopup(
logger.info("Sign in to GitHub and authorize Cloud Build GitHub app:");
const { url, cleanup } = await utils.openInBrowserPopup(
conn.installationState.actionUri,
"Authorize the GitHub app"
);
logger.info(`\t${url}`);
logger.info();
await promptOnce({
type: "input",
message: "Press Enter once you have authorized the app",
Expand Down Expand Up @@ -215,7 +247,7 @@ export async function getOrCreateConnection(
try {
conn = await gcb.getConnection(projectId, location, connectionId);
} catch (err: unknown) {
if ((err as FirebaseError).status === 404) {
if ((err as any).status === 404) {
conn = await createConnection(projectId, location, connectionId, githubConfig);
} else {
throw err;
Expand Down
13 changes: 9 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,10 @@ export async function openInBrowser(url: string): Promise<void> {
/**
* Like openInBrowser but opens the url in a popup.
*/
export async function openInBrowserPopup(url: string, buttonText: string): Promise<() => void> {
export async function openInBrowserPopup(
url: string,
buttonText: string
): Promise<{ url: string; cleanup: () => void }> {
const popupPage = fs
.readFileSync(path.join(__dirname, "../templates/popup.html"), { encoding: "utf-8" })
.replace("${url}", url)
Expand All @@ -787,10 +790,12 @@ export async function openInBrowserPopup(url: string, buttonText: string): Promi
server.listen(port);

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

return () => {
server.close();
return {
url: popupPageUri,
cleanup: () => {
server.close();
},
};
}

0 comments on commit 1a38ffb

Please sign in to comment.