Skip to content

Commit accf614

Browse files
authored
Provide firebase login credentials to Data connect emulator (#8342)
1 parent 57eed28 commit accf614

File tree

8 files changed

+88
-39
lines changed

8 files changed

+88
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Increase emulator UI body parser limit to match Storage emulator maximum. (#8329)
88
- Fixed Data Connect setup issues for fresh databases due to IAM user not being created. (#8335)
99
- Fixed an issue where `ext:install` used POSIX file seperators on Windows machines. (#8326)
10+
- Fixed an issue where credentials from `firebase login` would not be correctly provided to the Data Connect emulator.
1011
- Updated the Firebase Data Connect local toolkit to v1.9.1, which adds support for generated Angular SDKs and updates Dart SDK fields to follow best practices. (#8340)
1112
- Fixed misleading comments in `firebase init dataconnect` `connector.yaml` template.
1213
- Improved Data Connect SQL permissions to better handle tables owned by IAM roles. (#8339)

src/commands/dataconnect-sdk-generate.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { needProjectId } from "../projectUtils";
77
import { load } from "../dataconnect/load";
88
import { readFirebaseJson } from "../dataconnect/fileUtils";
99
import { logger } from "../logger";
10+
import { getProjectDefaultAccount } from "../auth";
1011

1112
type GenerateOptions = Options & { watch?: boolean };
1213

@@ -42,10 +43,12 @@ export const command = new Command("dataconnect:sdk:generate")
4243
return;
4344
}
4445
for (const conn of serviceInfo.connectorInfo) {
46+
const account = getProjectDefaultAccount(options.projectRoot);
4547
const output = await DataConnectEmulator.generate({
4648
configDir,
4749
connectorId: conn.connectorYaml.connectorId,
4850
watch: options.watch,
51+
account,
4952
});
5053
logger.info(output);
5154
logger.info(`Generated SDKs for ${conn.connectorYaml.connectorId}`);

src/dataconnect/build.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1-
import { DataConnectEmulator } from "../emulator/dataconnectEmulator";
1+
import { DataConnectBuildArgs, DataConnectEmulator } from "../emulator/dataconnectEmulator";
22
import { Options } from "../options";
33
import { FirebaseError } from "../error";
44
import * as experiments from "../experiments";
55
import { promptOnce } from "../prompt";
66
import * as utils from "../utils";
77
import { prettify, prettifyTable } from "./graphqlError";
88
import { DeploymentMetadata, GraphqlError } from "./types";
9+
import { getProjectDefaultAccount } from "../auth";
910

1011
export async function build(
1112
options: Options,
1213
configDir: string,
1314
dryRun?: boolean,
1415
): Promise<DeploymentMetadata> {
15-
const args: { configDir: string; projectId?: string } = { configDir };
16+
const account = getProjectDefaultAccount(options.projectRoot);
17+
const args: DataConnectBuildArgs = { configDir, account };
1618
if (experiments.isEnabled("fdcconnectorevolution") && options.projectId) {
1719
const flags = process.env["DATA_CONNECT_PREVIEW"];
1820
if (flags) {

src/defaultCredentials.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from "fs";
22
import * as path from "path";
3+
import { auth } from "google-auth-library";
34

45
import { clientId, clientSecret } from "./api";
56
import { Tokens, User, Account } from "./types/auth";
@@ -106,3 +107,12 @@ function userEmailSlug(user: User): string {
106107

107108
return slug;
108109
}
110+
111+
export async function hasDefaultCredentials(): Promise<boolean> {
112+
try {
113+
await auth.getApplicationDefault();
114+
return true;
115+
} catch (err: any) {
116+
return false;
117+
}
118+
}

src/emulator/dataconnectEmulator.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { Config } from "../config";
2828
import { PostgresServer, TRUNCATE_TABLES_SQL } from "./dataconnect/pgliteServer";
2929
import { cleanShutdown } from "./controller";
3030
import { connectableHostname } from "../utils";
31+
import { Account } from "../types/auth";
32+
import { getCredentialsEnvironment } from "./env";
3133

3234
export interface DataConnectEmulatorArgs {
3335
projectId: string;
@@ -43,17 +45,20 @@ export interface DataConnectEmulatorArgs {
4345
importPath?: string;
4446
debug?: boolean;
4547
extraEnv?: Record<string, string>;
48+
account?: Account;
4649
}
4750

4851
export interface DataConnectGenerateArgs {
4952
configDir: string;
5053
connectorId: string;
5154
watch?: boolean;
55+
account?: Account;
5256
}
5357

5458
export interface DataConnectBuildArgs {
5559
configDir: string;
5660
projectId?: string;
61+
account?: Account;
5762
}
5863

5964
// TODO: More concrete typing for events. Can we use string unions?
@@ -73,7 +78,10 @@ export class DataConnectEmulator implements EmulatorInstance {
7378
let resolvedConfigDir;
7479
try {
7580
resolvedConfigDir = this.args.config.path(this.args.configDir);
76-
const info = await DataConnectEmulator.build({ configDir: resolvedConfigDir });
81+
const info = await DataConnectEmulator.build({
82+
configDir: resolvedConfigDir,
83+
account: this.args.account,
84+
});
7785
if (requiresVector(info.metadata)) {
7886
if (Constants.isDemoProject(this.args.projectId)) {
7987
this.logger.logLabeled(
@@ -92,6 +100,7 @@ export class DataConnectEmulator implements EmulatorInstance {
92100
} catch (err: any) {
93101
this.logger.log("DEBUG", `'fdc build' failed with error: ${err.message}`);
94102
}
103+
const env = await DataConnectEmulator.getEnv(this.args.account, this.args.extraEnv);
95104
await start(
96105
Emulators.DATACONNECT,
97106
{
@@ -101,7 +110,7 @@ export class DataConnectEmulator implements EmulatorInstance {
101110
enable_output_schema_extensions: this.args.enable_output_schema_extensions,
102111
enable_output_generated_sdk: this.args.enable_output_generated_sdk,
103112
},
104-
this.args.extraEnv,
113+
env,
105114
);
106115

107116
this.usingExistingEmulator = false;
@@ -239,7 +248,8 @@ export class DataConnectEmulator implements EmulatorInstance {
239248
if (args.watch) {
240249
cmd.push("--watch");
241250
}
242-
const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8" });
251+
const env = await DataConnectEmulator.getEnv(args.account);
252+
const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8", env });
243253
if (isIncomaptibleArchError(res.error)) {
244254
throw new FirebaseError(
245255
`Unknown system error when running the Data Connect toolkit. ` +
@@ -267,8 +277,8 @@ export class DataConnectEmulator implements EmulatorInstance {
267277
if (args.projectId) {
268278
cmd.push(`--project_id=${args.projectId}`);
269279
}
270-
271-
const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8" });
280+
const env = await DataConnectEmulator.getEnv(args.account);
281+
const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8", env });
272282
if (isIncomaptibleArchError(res.error)) {
273283
throw new FirebaseError(
274284
`Unkown system error when running the Data Connect toolkit. ` +
@@ -341,6 +351,18 @@ export class DataConnectEmulator implements EmulatorInstance {
341351
}
342352
return false;
343353
}
354+
355+
static async getEnv(
356+
account?: Account,
357+
extraEnv: Record<string, string> = {},
358+
): Promise<NodeJS.ProcessEnv> {
359+
const credsEnv = await getCredentialsEnvironment(
360+
account,
361+
EmulatorLogger.forEmulator(Emulators.DATACONNECT),
362+
"dataconnect",
363+
);
364+
return { ...process.env, ...extraEnv, ...credsEnv };
365+
}
344366
}
345367

346368
type ConfigureEmulatorRequest = {

src/emulator/env.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Constants } from "./constants";
22
import { EmulatorInfo, Emulators } from "./types";
33
import { formatHost } from "./functionsEmulatorShared";
4+
import { Account } from "../types/auth/index";
5+
import { EmulatorLogger } from "./emulatorLogger";
6+
import { getCredentialPathAsync, hasDefaultCredentials } from "../defaultCredentials";
47

58
/**
69
* Adds or replaces emulator-related env vars (for Admin SDKs, etc.).
@@ -46,3 +49,29 @@ export function setEnvVarsForEmulators(
4649
}
4750
}
4851
}
52+
53+
/**
54+
* getCredentialsEnvironment returns any extra env vars beyond process.env that should be provided to emulators to ensure they have credentials.
55+
*/
56+
export async function getCredentialsEnvironment(
57+
account: Account | undefined,
58+
logger: EmulatorLogger,
59+
logLabel: string,
60+
): Promise<Record<string, string>> {
61+
// Provide default application credentials when appropriate
62+
const credentialEnv: Record<string, string> = {};
63+
if (await hasDefaultCredentials()) {
64+
logger.logLabeled(
65+
"WARN",
66+
logLabel,
67+
`Application Default Credentials detected. Non-emulated services will access production using these credentials. Be careful!`,
68+
);
69+
} else if (account) {
70+
const defaultCredPath = await getCredentialPathAsync(account);
71+
if (defaultCredPath) {
72+
logger.log("DEBUG", `Setting GAC to ${defaultCredPath}`);
73+
credentialEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath;
74+
}
75+
}
76+
return credentialEnv;
77+
}

src/emulator/functionsEmulator.ts

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ import { PubsubEmulator } from "./pubsubEmulator";
4545
import { FirebaseError } from "../error";
4646
import { WorkQueue, Work } from "./workQueue";
4747
import { allSettled, connectableHostname, createDestroyer, debounce, randomInt } from "../utils";
48-
import { getCredentialPathAsync } from "../defaultCredentials";
4948
import {
5049
AdminSdkConfig,
5150
constructDefaultAdminSdkConfig,
@@ -60,7 +59,7 @@ import * as functionsEnv from "../functions/env";
6059
import { AUTH_BLOCKING_EVENTS, BEFORE_CREATE_EVENT } from "../functions/events/v1";
6160
import { BlockingFunctionsConfig } from "../gcp/identityPlatform";
6261
import { resolveBackend } from "../deploy/functions/build";
63-
import { setEnvVarsForEmulators } from "./env";
62+
import { getCredentialsEnvironment, setEnvVarsForEmulators } from "./env";
6463
import { runWithVirtualEnv } from "../functions/python";
6564
import { Runtime } from "../deploy/functions/runtimes/supported";
6665
import { ExtensionsEmulator } from "./extensionsEmulator";
@@ -271,7 +270,11 @@ export class FunctionsEmulator implements EmulatorInstance {
271270
this.dynamicBackends =
272271
this.args.extensionsEmulator.filterUnemulatedTriggers(unfilteredBackends);
273272
const mode = this.debugMode ? FunctionsExecutionMode.SEQUENTIAL : FunctionsExecutionMode.AUTO;
274-
const credentialEnv = await this.getCredentialsEnvironment();
273+
const credentialEnv = await getCredentialsEnvironment(
274+
this.args.account,
275+
this.logger,
276+
"functions",
277+
);
275278
for (const backend of this.dynamicBackends) {
276279
backend.env = { ...credentialEnv, ...backend.env };
277280
if (this.workerPools[backend.codebase]) {
@@ -293,34 +296,6 @@ export class FunctionsEmulator implements EmulatorInstance {
293296
}
294297
}
295298

296-
private async getCredentialsEnvironment(): Promise<Record<string, string>> {
297-
// Provide default application credentials when appropriate
298-
const credentialEnv: Record<string, string> = {};
299-
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
300-
this.logger.logLabeled(
301-
"WARN",
302-
"functions",
303-
`Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to ${process.env.GOOGLE_APPLICATION_CREDENTIALS}. Non-emulated services will access production using these credentials. Be careful!`,
304-
);
305-
} else if (this.args.account) {
306-
const defaultCredPath = await getCredentialPathAsync(this.args.account);
307-
if (defaultCredPath) {
308-
this.logger.log("DEBUG", `Setting GAC to ${defaultCredPath}`);
309-
credentialEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath;
310-
}
311-
} else {
312-
// TODO: It would be safer to set GOOGLE_APPLICATION_CREDENTIALS to /dev/null here but we can't because some SDKs don't work
313-
// without credentials even when talking to the emulator: https://github.com/firebase/firebase-js-sdk/issues/3144
314-
this.logger.logLabeled(
315-
"WARN",
316-
"functions",
317-
"You are not signed in to the Firebase CLI. If you have authorized this machine using gcloud application-default credentials those may be discovered and used to access production services.",
318-
);
319-
}
320-
321-
return credentialEnv;
322-
}
323-
324299
createHubServer(): express.Application {
325300
// TODO(samstern): Should not need this here but some tests are directly calling this method
326301
// because FunctionsEmulator.start() used to not be test safe.
@@ -458,7 +433,11 @@ export class FunctionsEmulator implements EmulatorInstance {
458433
}
459434

460435
async start(): Promise<void> {
461-
const credentialEnv = await this.getCredentialsEnvironment();
436+
const credentialEnv = await getCredentialsEnvironment(
437+
this.args.account,
438+
this.logger,
439+
"functions",
440+
);
462441
for (const e of this.staticBackends) {
463442
e.env = { ...credentialEnv, ...e.env };
464443
}

src/init/features/dataconnect/sdk.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator";
2828
import { FirebaseError } from "../../../error";
2929
import { camelCase, snakeCase, upperFirst } from "lodash";
3030
import { logSuccess, logBullet } from "../../../utils";
31+
import { getGlobalDefaultAccount } from "../../../auth";
3132

3233
export const FDC_APP_FOLDER = "_FDC_APP_FOLDER";
3334
export type SDKInfo = {
@@ -227,9 +228,11 @@ export async function actuate(sdkInfo: SDKInfo) {
227228
const connectorYamlPath = `${sdkInfo.connectorInfo.directory}/connector.yaml`;
228229
fs.writeFileSync(connectorYamlPath, sdkInfo.connectorYamlContents, "utf8");
229230
logBullet(`Wrote new config to ${connectorYamlPath}`);
231+
const account = getGlobalDefaultAccount();
230232
await DataConnectEmulator.generate({
231233
configDir: sdkInfo.connectorInfo.directory,
232234
connectorId: sdkInfo.connectorInfo.connectorYaml.connectorId,
235+
account,
233236
});
234237
logBullet(`Generated SDK code for ${sdkInfo.connectorInfo.connectorYaml.connectorId}`);
235238
if (sdkInfo.connectorInfo.connectorYaml.generate?.swiftSdk && sdkInfo.displayIOSWarning) {

0 commit comments

Comments
 (0)