diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4e570440a..a208d1317ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Increase emulator UI body parser limit to match Storage emulator maximum. (#8329) - Fixed Data Connect setup issues for fresh databases due to IAM user not being created. (#8335) - Fixed an issue where `ext:install` used POSIX file seperators on Windows machines. (#8326) +- Fixed an issue where credentials from `firebase login` would not be correctly provided to the Data Connect emulator. - 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) - Fixed misleading comments in `firebase init dataconnect` `connector.yaml` template. - Improved Data Connect SQL permissions to better handle tables owned by IAM roles. (#8339) diff --git a/src/commands/dataconnect-sdk-generate.ts b/src/commands/dataconnect-sdk-generate.ts index 00c27c623f1..d522af7d4fa 100644 --- a/src/commands/dataconnect-sdk-generate.ts +++ b/src/commands/dataconnect-sdk-generate.ts @@ -7,6 +7,7 @@ import { needProjectId } from "../projectUtils"; import { load } from "../dataconnect/load"; import { readFirebaseJson } from "../dataconnect/fileUtils"; import { logger } from "../logger"; +import { getProjectDefaultAccount } from "../auth"; type GenerateOptions = Options & { watch?: boolean }; @@ -42,10 +43,12 @@ export const command = new Command("dataconnect:sdk:generate") return; } for (const conn of serviceInfo.connectorInfo) { + const account = getProjectDefaultAccount(options.projectRoot); const output = await DataConnectEmulator.generate({ configDir, connectorId: conn.connectorYaml.connectorId, watch: options.watch, + account, }); logger.info(output); logger.info(`Generated SDKs for ${conn.connectorYaml.connectorId}`); diff --git a/src/dataconnect/build.ts b/src/dataconnect/build.ts index 2dfe7e8f832..acad16ca04e 100644 --- a/src/dataconnect/build.ts +++ b/src/dataconnect/build.ts @@ -1,4 +1,4 @@ -import { DataConnectEmulator } from "../emulator/dataconnectEmulator"; +import { DataConnectBuildArgs, DataConnectEmulator } from "../emulator/dataconnectEmulator"; import { Options } from "../options"; import { FirebaseError } from "../error"; import * as experiments from "../experiments"; @@ -6,13 +6,15 @@ import { promptOnce } from "../prompt"; import * as utils from "../utils"; import { prettify, prettifyTable } from "./graphqlError"; import { DeploymentMetadata, GraphqlError } from "./types"; +import { getProjectDefaultAccount } from "../auth"; export async function build( options: Options, configDir: string, dryRun?: boolean, ): Promise { - const args: { configDir: string; projectId?: string } = { configDir }; + const account = getProjectDefaultAccount(options.projectRoot); + const args: DataConnectBuildArgs = { configDir, account }; if (experiments.isEnabled("fdcconnectorevolution") && options.projectId) { const flags = process.env["DATA_CONNECT_PREVIEW"]; if (flags) { diff --git a/src/defaultCredentials.ts b/src/defaultCredentials.ts index 7a881213add..7ca6d7bec62 100644 --- a/src/defaultCredentials.ts +++ b/src/defaultCredentials.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; import * as path from "path"; +import { auth } from "google-auth-library"; import { clientId, clientSecret } from "./api"; import { Tokens, User, Account } from "./types/auth"; @@ -106,3 +107,12 @@ function userEmailSlug(user: User): string { return slug; } + +export async function hasDefaultCredentials(): Promise { + try { + await auth.getApplicationDefault(); + return true; + } catch (err: any) { + return false; + } +} diff --git a/src/emulator/dataconnectEmulator.ts b/src/emulator/dataconnectEmulator.ts index 5aac8521d83..7ec1b70d971 100644 --- a/src/emulator/dataconnectEmulator.ts +++ b/src/emulator/dataconnectEmulator.ts @@ -28,6 +28,8 @@ import { Config } from "../config"; import { PostgresServer, TRUNCATE_TABLES_SQL } from "./dataconnect/pgliteServer"; import { cleanShutdown } from "./controller"; import { connectableHostname } from "../utils"; +import { Account } from "../types/auth"; +import { getCredentialsEnvironment } from "./env"; export interface DataConnectEmulatorArgs { projectId: string; @@ -43,17 +45,20 @@ export interface DataConnectEmulatorArgs { importPath?: string; debug?: boolean; extraEnv?: Record; + account?: Account; } export interface DataConnectGenerateArgs { configDir: string; connectorId: string; watch?: boolean; + account?: Account; } export interface DataConnectBuildArgs { configDir: string; projectId?: string; + account?: Account; } // TODO: More concrete typing for events. Can we use string unions? @@ -73,7 +78,10 @@ export class DataConnectEmulator implements EmulatorInstance { let resolvedConfigDir; try { resolvedConfigDir = this.args.config.path(this.args.configDir); - const info = await DataConnectEmulator.build({ configDir: resolvedConfigDir }); + const info = await DataConnectEmulator.build({ + configDir: resolvedConfigDir, + account: this.args.account, + }); if (requiresVector(info.metadata)) { if (Constants.isDemoProject(this.args.projectId)) { this.logger.logLabeled( @@ -92,6 +100,7 @@ export class DataConnectEmulator implements EmulatorInstance { } catch (err: any) { this.logger.log("DEBUG", `'fdc build' failed with error: ${err.message}`); } + const env = await DataConnectEmulator.getEnv(this.args.account, this.args.extraEnv); await start( Emulators.DATACONNECT, { @@ -101,7 +110,7 @@ export class DataConnectEmulator implements EmulatorInstance { enable_output_schema_extensions: this.args.enable_output_schema_extensions, enable_output_generated_sdk: this.args.enable_output_generated_sdk, }, - this.args.extraEnv, + env, ); this.usingExistingEmulator = false; @@ -239,7 +248,8 @@ export class DataConnectEmulator implements EmulatorInstance { if (args.watch) { cmd.push("--watch"); } - const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8" }); + const env = await DataConnectEmulator.getEnv(args.account); + const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8", env }); if (isIncomaptibleArchError(res.error)) { throw new FirebaseError( `Unknown system error when running the Data Connect toolkit. ` + @@ -267,8 +277,8 @@ export class DataConnectEmulator implements EmulatorInstance { if (args.projectId) { cmd.push(`--project_id=${args.projectId}`); } - - const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8" }); + const env = await DataConnectEmulator.getEnv(args.account); + const res = childProcess.spawnSync(commandInfo.binary, cmd, { encoding: "utf-8", env }); if (isIncomaptibleArchError(res.error)) { throw new FirebaseError( `Unkown system error when running the Data Connect toolkit. ` + @@ -341,6 +351,18 @@ export class DataConnectEmulator implements EmulatorInstance { } return false; } + + static async getEnv( + account?: Account, + extraEnv: Record = {}, + ): Promise { + const credsEnv = await getCredentialsEnvironment( + account, + EmulatorLogger.forEmulator(Emulators.DATACONNECT), + "dataconnect", + ); + return { ...process.env, ...extraEnv, ...credsEnv }; + } } type ConfigureEmulatorRequest = { diff --git a/src/emulator/env.ts b/src/emulator/env.ts index 81cd6f023c3..bc2a2f147b0 100644 --- a/src/emulator/env.ts +++ b/src/emulator/env.ts @@ -1,6 +1,9 @@ import { Constants } from "./constants"; import { EmulatorInfo, Emulators } from "./types"; import { formatHost } from "./functionsEmulatorShared"; +import { Account } from "../types/auth/index"; +import { EmulatorLogger } from "./emulatorLogger"; +import { getCredentialPathAsync, hasDefaultCredentials } from "../defaultCredentials"; /** * Adds or replaces emulator-related env vars (for Admin SDKs, etc.). @@ -46,3 +49,29 @@ export function setEnvVarsForEmulators( } } } + +/** + * getCredentialsEnvironment returns any extra env vars beyond process.env that should be provided to emulators to ensure they have credentials. + */ +export async function getCredentialsEnvironment( + account: Account | undefined, + logger: EmulatorLogger, + logLabel: string, +): Promise> { + // Provide default application credentials when appropriate + const credentialEnv: Record = {}; + if (await hasDefaultCredentials()) { + logger.logLabeled( + "WARN", + logLabel, + `Application Default Credentials detected. Non-emulated services will access production using these credentials. Be careful!`, + ); + } else if (account) { + const defaultCredPath = await getCredentialPathAsync(account); + if (defaultCredPath) { + logger.log("DEBUG", `Setting GAC to ${defaultCredPath}`); + credentialEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath; + } + } + return credentialEnv; +} diff --git a/src/emulator/functionsEmulator.ts b/src/emulator/functionsEmulator.ts index fca20c598a8..7743ac38b0c 100644 --- a/src/emulator/functionsEmulator.ts +++ b/src/emulator/functionsEmulator.ts @@ -45,7 +45,6 @@ import { PubsubEmulator } from "./pubsubEmulator"; import { FirebaseError } from "../error"; import { WorkQueue, Work } from "./workQueue"; import { allSettled, connectableHostname, createDestroyer, debounce, randomInt } from "../utils"; -import { getCredentialPathAsync } from "../defaultCredentials"; import { AdminSdkConfig, constructDefaultAdminSdkConfig, @@ -60,7 +59,7 @@ import * as functionsEnv from "../functions/env"; import { AUTH_BLOCKING_EVENTS, BEFORE_CREATE_EVENT } from "../functions/events/v1"; import { BlockingFunctionsConfig } from "../gcp/identityPlatform"; import { resolveBackend } from "../deploy/functions/build"; -import { setEnvVarsForEmulators } from "./env"; +import { getCredentialsEnvironment, setEnvVarsForEmulators } from "./env"; import { runWithVirtualEnv } from "../functions/python"; import { Runtime } from "../deploy/functions/runtimes/supported"; import { ExtensionsEmulator } from "./extensionsEmulator"; @@ -271,7 +270,11 @@ export class FunctionsEmulator implements EmulatorInstance { this.dynamicBackends = this.args.extensionsEmulator.filterUnemulatedTriggers(unfilteredBackends); const mode = this.debugMode ? FunctionsExecutionMode.SEQUENTIAL : FunctionsExecutionMode.AUTO; - const credentialEnv = await this.getCredentialsEnvironment(); + const credentialEnv = await getCredentialsEnvironment( + this.args.account, + this.logger, + "functions", + ); for (const backend of this.dynamicBackends) { backend.env = { ...credentialEnv, ...backend.env }; if (this.workerPools[backend.codebase]) { @@ -293,34 +296,6 @@ export class FunctionsEmulator implements EmulatorInstance { } } - private async getCredentialsEnvironment(): Promise> { - // Provide default application credentials when appropriate - const credentialEnv: Record = {}; - if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { - this.logger.logLabeled( - "WARN", - "functions", - `Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to ${process.env.GOOGLE_APPLICATION_CREDENTIALS}. Non-emulated services will access production using these credentials. Be careful!`, - ); - } else if (this.args.account) { - const defaultCredPath = await getCredentialPathAsync(this.args.account); - if (defaultCredPath) { - this.logger.log("DEBUG", `Setting GAC to ${defaultCredPath}`); - credentialEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath; - } - } else { - // TODO: It would be safer to set GOOGLE_APPLICATION_CREDENTIALS to /dev/null here but we can't because some SDKs don't work - // without credentials even when talking to the emulator: https://github.com/firebase/firebase-js-sdk/issues/3144 - this.logger.logLabeled( - "WARN", - "functions", - "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.", - ); - } - - return credentialEnv; - } - createHubServer(): express.Application { // TODO(samstern): Should not need this here but some tests are directly calling this method // because FunctionsEmulator.start() used to not be test safe. @@ -458,7 +433,11 @@ export class FunctionsEmulator implements EmulatorInstance { } async start(): Promise { - const credentialEnv = await this.getCredentialsEnvironment(); + const credentialEnv = await getCredentialsEnvironment( + this.args.account, + this.logger, + "functions", + ); for (const e of this.staticBackends) { e.env = { ...credentialEnv, ...e.env }; } diff --git a/src/init/features/dataconnect/sdk.ts b/src/init/features/dataconnect/sdk.ts index afa9f5e6b9b..89b8b29823d 100644 --- a/src/init/features/dataconnect/sdk.ts +++ b/src/init/features/dataconnect/sdk.ts @@ -28,6 +28,7 @@ import { DataConnectEmulator } from "../../../emulator/dataconnectEmulator"; import { FirebaseError } from "../../../error"; import { camelCase, snakeCase, upperFirst } from "lodash"; import { logSuccess, logBullet } from "../../../utils"; +import { getGlobalDefaultAccount } from "../../../auth"; export const FDC_APP_FOLDER = "_FDC_APP_FOLDER"; export type SDKInfo = { @@ -227,9 +228,11 @@ export async function actuate(sdkInfo: SDKInfo) { const connectorYamlPath = `${sdkInfo.connectorInfo.directory}/connector.yaml`; fs.writeFileSync(connectorYamlPath, sdkInfo.connectorYamlContents, "utf8"); logBullet(`Wrote new config to ${connectorYamlPath}`); + const account = getGlobalDefaultAccount(); await DataConnectEmulator.generate({ configDir: sdkInfo.connectorInfo.directory, connectorId: sdkInfo.connectorInfo.connectorYaml.connectorId, + account, }); logBullet(`Generated SDK code for ${sdkInfo.connectorInfo.connectorYaml.connectorId}`); if (sdkInfo.connectorInfo.connectorYaml.generate?.swiftSdk && sdkInfo.displayIOSWarning) {