Skip to content

Commit ee68b28

Browse files
authoredApr 2, 2024··
Move FAH files from init directory to common library as there is no init command (#6945)
1 parent db7dd17 commit ee68b28

15 files changed

+236
-271
lines changed
 
File renamed without changes.

‎src/init/features/apphosting/githubConnections.ts ‎src/apphosting/githubConnections.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as clc from "colorette";
22

3-
import * as devConnect from "../../../gcp/devConnect";
4-
import * as rm from "../../../gcp/resourceManager";
5-
import * as poller from "../../../operation-poller";
6-
import * as utils from "../../../utils";
7-
import { FirebaseError } from "../../../error";
8-
import { promptOnce } from "../../../prompt";
9-
import { getProjectNumber } from "../../../getProjectNumber";
10-
import { developerConnectOrigin } from "../../../api";
3+
import * as devConnect from "../gcp/devConnect";
4+
import * as rm from "../gcp/resourceManager";
5+
import * as poller from "../operation-poller";
6+
import * as utils from "../utils";
7+
import { FirebaseError } from "../error";
8+
import { promptOnce } from "../prompt";
9+
import { getProjectNumber } from "../getProjectNumber";
10+
import { developerConnectOrigin } from "../api";
1111

1212
import * as fuzzy from "fuzzy";
1313
import * as inquirer from "inquirer";

‎src/init/features/apphosting/index.ts ‎src/apphosting/index.ts

+14-20
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
import * as clc from "colorette";
22

33
import * as repo from "./repo";
4-
import * as poller from "../../../operation-poller";
5-
import * as apphosting from "../../../gcp/apphosting";
4+
import * as poller from "../operation-poller";
5+
import * as apphosting from "../gcp/apphosting";
66
import * as githubConnections from "./githubConnections";
7-
import { logBullet, logSuccess, logWarning } from "../../../utils";
7+
import { logBullet, logSuccess, logWarning } from "../utils";
88
import {
99
apphostingOrigin,
1010
artifactRegistryDomain,
1111
cloudRunApiOrigin,
1212
cloudbuildOrigin,
1313
developerConnectOrigin,
1414
secretManagerOrigin,
15-
} from "../../../api";
16-
import {
17-
Backend,
18-
BackendOutputOnlyFields,
19-
API_VERSION,
20-
Build,
21-
Rollout,
22-
} from "../../../gcp/apphosting";
23-
import { addServiceAccountToRoles } from "../../../gcp/resourceManager";
24-
import * as iam from "../../../gcp/iam";
25-
import { Repository } from "../../../gcp/cloudbuild";
26-
import { FirebaseError } from "../../../error";
27-
import { promptOnce } from "../../../prompt";
15+
} from "../api";
16+
import { Backend, BackendOutputOnlyFields, API_VERSION, Build, Rollout } from "../gcp/apphosting";
17+
import { addServiceAccountToRoles } from "../gcp/resourceManager";
18+
import * as iam from "../gcp/iam";
19+
import { Repository } from "../gcp/cloudbuild";
20+
import { FirebaseError } from "../error";
21+
import { promptOnce } from "../prompt";
2822
import { DEFAULT_REGION } from "./constants";
29-
import { ensure } from "../../../ensureApiEnabled";
30-
import * as deploymentTool from "../../../deploymentTool";
31-
import { DeepOmit } from "../../../metaprogramming";
32-
import { GitRepositoryLink } from "../../../gcp/devConnect";
23+
import { ensure } from "../ensureApiEnabled";
24+
import * as deploymentTool from "../deploymentTool";
25+
import { DeepOmit } from "../metaprogramming";
26+
import { GitRepositoryLink } from "../gcp/devConnect";
3327

3428
const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
3529

‎src/init/features/apphosting/repo.ts ‎src/apphosting/repo.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as clc from "colorette";
22

3-
import * as gcb from "../../../gcp/cloudbuild";
4-
import * as rm from "../../../gcp/resourceManager";
5-
import * as poller from "../../../operation-poller";
6-
import * as utils from "../../../utils";
7-
import { cloudbuildOrigin } from "../../../api";
8-
import { FirebaseError } from "../../../error";
9-
import { promptOnce } from "../../../prompt";
10-
import { getProjectNumber } from "../../../getProjectNumber";
3+
import * as gcb from "../gcp/cloudbuild";
4+
import * as rm from "../gcp/resourceManager";
5+
import * as poller from "../operation-poller";
6+
import * as utils from "../utils";
7+
import { cloudbuildOrigin } from "../api";
8+
import { FirebaseError } from "../error";
9+
import { promptOnce } from "../prompt";
10+
import { getProjectNumber } from "../getProjectNumber";
1111

1212
import * as fuzzy from "fuzzy";
1313
import * as inquirer from "inquirer";

‎src/apphosting/secrets/index.ts

+89
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,99 @@
11
import { FirebaseError } from "../../error";
2+
import * as iam from "../../gcp/iam";
23
import * as gcsm from "../../gcp/secretManager";
4+
import * as gcb from "../../gcp/cloudbuild";
5+
import * as gce from "../../gcp/computeEngine";
36
import { FIREBASE_MANAGED } from "../../gcp/secretManager";
47
import { isFunctionsManaged } from "../../gcp/secretManager";
58
import * as utils from "../../utils";
69
import * as prompt from "../../prompt";
710

11+
function fetchServiceAccounts(projectNumber: string): {
12+
buildServiceAccount: string;
13+
runServiceAccount: string;
14+
} {
15+
// TODO: For now we will always return the default CBSA and CESA. When the getBackend call supports returning
16+
// the attached service account in a given backend/location then return that value instead.
17+
// Sample Call: await apphosting.getBackend(projectId, location, backendId); & make this function async
18+
return {
19+
buildServiceAccount: gcb.getDefaultServiceAccount(projectNumber),
20+
runServiceAccount: gce.getDefaultServiceAccount(projectNumber),
21+
};
22+
}
23+
24+
/**
25+
* Grants the corresponding service accounts the necessary access permissions to the provided secret.
26+
*/
27+
export async function grantSecretAccess(
28+
secretName: string,
29+
location: string,
30+
backendId: string,
31+
projectId: string,
32+
projectNumber: string,
33+
): Promise<void> {
34+
const isExist = await gcsm.secretExists(projectId, secretName);
35+
if (!isExist) {
36+
throw new FirebaseError(`Secret ${secretName} does not exist in project ${projectId}`);
37+
}
38+
39+
let serviceAccounts = { buildServiceAccount: "", runServiceAccount: "" };
40+
try {
41+
serviceAccounts = fetchServiceAccounts(projectNumber);
42+
} catch (err: any) {
43+
throw new FirebaseError(
44+
`Failed to get backend ${backendId} at location ${location}. Please check the parameters you have provided.`,
45+
{ original: err },
46+
);
47+
}
48+
49+
const secret = {
50+
projectId: projectId,
51+
name: secretName,
52+
};
53+
54+
// TODO: Document why Cloud Build SA needs viewer permission but Run doesn't.
55+
// TODO: future proof for when therte is a single service account (currently will set the same
56+
// secretAccessor permission twice)
57+
const newBindings: iam.Binding[] = [
58+
{
59+
role: "roles/secretmanager.secretAccessor",
60+
members: [
61+
`serviceAccount:${serviceAccounts.buildServiceAccount}`,
62+
`serviceAccount:${serviceAccounts.runServiceAccount}`,
63+
],
64+
},
65+
// Cloud Build needs the viewer role so that it can list secret versions and pin the Build to the
66+
// latest version.
67+
{
68+
role: "roles/secretmanager.viewer",
69+
members: [`serviceAccount:${serviceAccounts.buildServiceAccount}`],
70+
},
71+
];
72+
73+
let existingBindings;
74+
try {
75+
existingBindings = (await gcsm.getIamPolicy(secret)).bindings;
76+
} catch (err: any) {
77+
throw new FirebaseError(
78+
`Failed to get IAM bindings on secret: ${secret.name}. Ensure you have the permissions to do so and try again.`,
79+
{ original: err },
80+
);
81+
}
82+
83+
try {
84+
// TODO: Merge with existing bindings with the same role
85+
const updatedBindings = existingBindings.concat(newBindings);
86+
await gcsm.setIamPolicy(secret, updatedBindings);
87+
} catch (err: any) {
88+
throw new FirebaseError(
89+
`Failed to set IAM bindings ${JSON.stringify(newBindings)} on secret: ${secret.name}. Ensure you have the permissions to do so and try again.`,
90+
{ original: err },
91+
);
92+
}
93+
94+
utils.logSuccess(`Successfully set IAM bindings on secret ${secret.name}.\n`);
95+
}
96+
897
/**
998
* Ensures a secret exists for use with app hosting, optionally locked to a region.
1099
* If a secret exists, we verify the user is not trying to change the region and verifies a secret

‎src/commands/apphosting-backends-create.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Command } from "../command";
22
import { Options } from "../options";
33
import { needProjectId } from "../projectUtils";
44
import requireInteractive from "../requireInteractive";
5-
import { doSetup } from "../init/features/apphosting";
5+
import { doSetup } from "../apphosting";
66
import { ensureApiEnabled } from "../gcp/apphosting";
77

88
export const command = new Command("apphosting:backends:create")

‎src/commands/apphosting-backends-delete.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Options } from "../options";
33
import { needProjectId } from "../projectUtils";
44
import { FirebaseError } from "../error";
55
import { promptOnce } from "../prompt";
6-
import { DEFAULT_REGION } from "../init/features/apphosting/constants";
6+
import { DEFAULT_REGION } from "../apphosting/constants";
77
import * as utils from "../utils";
88
import * as apphosting from "../gcp/apphosting";
99
import { printBackendsTable } from "./apphosting-backends-list";

‎src/commands/apphosting-secrets-grantaccess.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { requireAuth } from "../requireAuth";
66
import * as secretManager from "../gcp/secretManager";
77
import { requirePermissions } from "../requirePermissions";
88
import * as apphosting from "../gcp/apphosting";
9-
import { grantSecretAccess } from "../init/features/apphosting/secrets";
9+
import { grantSecretAccess } from "../apphosting/secrets";
1010

1111
export const command = new Command("apphosting:secrets:grantaccess <secretName>")
1212
.description("grant service accounts permissions to the provided secret")

‎src/init/features/apphosting/secrets.ts

-92
This file was deleted.

‎src/init/features/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ export { doSetup as extensions } from "./extensions";
1010
export { doSetup as project } from "./project";
1111
export { doSetup as remoteconfig } from "./remoteconfig";
1212
export { initGitHub as hostingGithub } from "./hosting/github";
13-
export { doSetup as apphosting } from "./apphosting";
13+
export { doSetup as apphosting } from "../../apphosting";

‎src/test/init/apphosting/githubConnections.spec.ts ‎src/test/apphosting/githubConnections.spec.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as sinon from "sinon";
22
import { expect } from "chai";
3-
import * as prompt from "../../../prompt";
4-
import * as poller from "../../../operation-poller";
5-
import * as devconnect from "../../../gcp/devConnect";
6-
import * as repo from "../../../init/features/apphosting/githubConnections";
7-
import * as utils from "../../../utils";
8-
import * as srcUtils from "../../../../src/getProjectNumber";
9-
import * as rm from "../../../gcp/resourceManager";
10-
import { FirebaseError } from "../../../error";
3+
import * as prompt from "../../prompt";
4+
import * as poller from "../../operation-poller";
5+
import * as devconnect from "../../gcp/devConnect";
6+
import * as repo from "../../apphosting/githubConnections";
7+
import * as utils from "../../utils";
8+
import * as srcUtils from "../../getProjectNumber";
9+
import * as rm from "../../gcp/resourceManager";
10+
import { FirebaseError } from "../../error";
1111

1212
const projectId = "projectId";
1313
const location = "us-central1";

‎src/test/init/apphosting/index.spec.ts ‎src/test/apphosting/index.spec.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as sinon from "sinon";
22
import { expect } from "chai";
33

4-
import * as apphosting from "../../../gcp/apphosting";
5-
import * as iam from "../../../gcp/iam";
6-
import * as resourceManager from "../../../gcp/resourceManager";
7-
import * as poller from "../../../operation-poller";
8-
import { createBackend, setDefaultTrafficPolicy } from "../../../init/features/apphosting/index";
9-
import * as deploymentTool from "../../../deploymentTool";
10-
import { FirebaseError } from "../../../error";
4+
import * as apphosting from "../../gcp/apphosting";
5+
import * as iam from "../../gcp/iam";
6+
import * as resourceManager from "../../gcp/resourceManager";
7+
import * as poller from "../../operation-poller";
8+
import { createBackend, setDefaultTrafficPolicy } from "../../apphosting/index";
9+
import * as deploymentTool from "../../deploymentTool";
10+
import { FirebaseError } from "../../error";
1111

1212
describe("operationsConverter", () => {
1313
const sandbox: sinon.SinonSandbox = sinon.createSandbox();

‎src/test/init/apphosting/repo.spec.ts ‎src/test/apphosting/repo.spec.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as sinon from "sinon";
22
import { expect } from "chai";
33

4-
import * as gcb from "../../../gcp/cloudbuild";
5-
import * as rm from "../../../gcp/resourceManager";
6-
import * as prompt from "../../../prompt";
7-
import * as poller from "../../../operation-poller";
8-
import * as repo from "../../../init/features/apphosting/repo";
9-
import * as utils from "../../../utils";
10-
import * as srcUtils from "../../../../src/getProjectNumber";
11-
import { FirebaseError } from "../../../error";
4+
import * as gcb from "../../gcp/cloudbuild";
5+
import * as rm from "../../gcp/resourceManager";
6+
import * as prompt from "../../prompt";
7+
import * as poller from "../../operation-poller";
8+
import * as repo from "../../apphosting/repo";
9+
import * as utils from "../../utils";
10+
import * as srcUtils from "../../getProjectNumber";
11+
import { FirebaseError } from "../../error";
1212

1313
const projectId = "projectId";
1414
const location = "us-central1";

‎src/test/apphosting/secrets/index.spec.ts

+90-15
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,35 @@ import { expect } from "chai";
22
import * as sinon from "sinon";
33

44
import * as secrets from "../../../apphosting/secrets";
5+
import * as iam from "../../../gcp/iam";
6+
import * as gcb from "../../../gcp/cloudbuild";
7+
import * as gce from "../../../gcp/computeEngine";
58
import * as gcsmImport from "../../../gcp/secretManager";
69
import * as utilsImport from "../../../utils";
710
import * as promptImport from "../../../prompt";
11+
import { FirebaseError } from "../../../error";
812

913
describe("secrets", () => {
10-
describe("upsertSecret", () => {
11-
let gcsm: sinon.SinonStubbedInstance<typeof gcsmImport>;
12-
let utils: sinon.SinonStubbedInstance<typeof utilsImport>;
13-
let prompt: sinon.SinonStubbedInstance<typeof promptImport>;
14-
15-
beforeEach(() => {
16-
gcsm = sinon.stub(gcsmImport);
17-
utils = sinon.stub(utilsImport);
18-
prompt = sinon.stub(promptImport);
19-
gcsm.isFunctionsManaged.restore();
20-
gcsm.labels.restore();
21-
});
14+
let gcsm: sinon.SinonStubbedInstance<typeof gcsmImport>;
15+
let utils: sinon.SinonStubbedInstance<typeof utilsImport>;
16+
let prompt: sinon.SinonStubbedInstance<typeof promptImport>;
2217

23-
afterEach(() => {
24-
sinon.verifyAndRestore();
25-
});
18+
beforeEach(() => {
19+
gcsm = sinon.stub(gcsmImport);
20+
utils = sinon.stub(utilsImport);
21+
prompt = sinon.stub(promptImport);
22+
gcsm.isFunctionsManaged.restore();
23+
gcsm.labels.restore();
24+
gcsm.secretExists.throws("Unexpected secretExists call");
25+
gcsm.getIamPolicy.throws("Unexpected getIamPolicy call");
26+
gcsm.setIamPolicy.throws("Unexpected setIamPolicy call");
27+
});
28+
29+
afterEach(() => {
30+
sinon.verifyAndRestore();
31+
});
2632

33+
describe("upsertSecret", () => {
2734
it("errors if a user tries to change replication policies (was global)", async () => {
2835
gcsm.getSecret.withArgs("project", "secret").resolves({
2936
name: "secret",
@@ -159,4 +166,72 @@ describe("secrets", () => {
159166
);
160167
});
161168
});
169+
170+
describe("grantSecretAccess", () => {
171+
const projectId = "projectId";
172+
const projectNumber = "123456789";
173+
const location = "us-central1";
174+
const backendId = "backendId";
175+
const secretName = "secretName";
176+
const existingPolicy: iam.Policy = {
177+
version: 1,
178+
etag: "tag",
179+
bindings: [
180+
{
181+
role: "roles/viewer",
182+
members: [`serviceAccount:${gce.getDefaultServiceAccount(projectNumber)}`],
183+
},
184+
],
185+
};
186+
187+
it("should grant access to the appropriate service accounts", async () => {
188+
gcsm.secretExists.resolves(true);
189+
gcsm.getIamPolicy.resolves(existingPolicy);
190+
gcsm.setIamPolicy.resolves();
191+
192+
await secrets.grantSecretAccess(secretName, location, backendId, projectId, projectNumber);
193+
194+
const secret = {
195+
projectId: projectId,
196+
name: secretName,
197+
};
198+
199+
const newBindings: iam.Binding[] = [
200+
{
201+
role: "roles/viewer",
202+
members: [`serviceAccount:${gce.getDefaultServiceAccount(projectNumber)}`],
203+
},
204+
{
205+
role: "roles/secretmanager.secretAccessor",
206+
members: [
207+
`serviceAccount:${gcb.getDefaultServiceAccount(projectNumber)}`,
208+
`serviceAccount:${gce.getDefaultServiceAccount(projectNumber)}`,
209+
],
210+
},
211+
{
212+
role: "roles/secretmanager.viewer",
213+
members: [`serviceAccount:${gcb.getDefaultServiceAccount(projectNumber)}`],
214+
},
215+
];
216+
217+
expect(gcsm.secretExists).to.be.calledWith(projectId, secretName);
218+
expect(gcsm.getIamPolicy).to.be.calledWith(secret);
219+
expect(gcsm.setIamPolicy).to.be.calledWith(secret, newBindings);
220+
});
221+
222+
it("does not grant access to a secret that doesn't exist", () => {
223+
gcsm.secretExists.resolves(false);
224+
225+
expect(
226+
secrets.grantSecretAccess(secretName, location, backendId, projectId, projectNumber),
227+
).to.be.rejectedWith(
228+
FirebaseError,
229+
`Secret ${secretName} does not exist in project ${projectId}`,
230+
);
231+
232+
expect(gcsm.secretExists).to.be.calledWith(projectId, secretName);
233+
expect(gcsm.secretExists).to.be.calledOnce;
234+
expect(gcsm.setIamPolicy).to.not.have.been.called;
235+
});
236+
});
162237
});

‎src/test/init/apphosting/secrets.spec.ts

-101
This file was deleted.

0 commit comments

Comments
 (0)
Please sign in to comment.