firebase-tools
Version:
Command-Line Interface for Firebase
231 lines (230 loc) • 10.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.toMulti = toMulti;
exports.serviceAccountsForBackend = serviceAccountsForBackend;
exports.grantSecretAccess = grantSecretAccess;
exports.grantEmailsSecretAccess = grantEmailsSecretAccess;
exports.upsertSecret = upsertSecret;
exports.fetchSecrets = fetchSecrets;
exports.getSecretNameParts = getSecretNameParts;
exports.apphostingSecretsSetAction = apphostingSecretsSetAction;
const clc = require("colorette");
const error_1 = require("../../error");
const gcsm = require("../../gcp/secretManager");
const gcb = require("../../gcp/cloudbuild");
const gce = require("../../gcp/computeEngine");
const apphosting = require("../../gcp/apphosting");
const secretManager_1 = require("../../gcp/secretManager");
const secretManager_2 = require("../../gcp/secretManager");
const utils = require("../../utils");
const prompt = require("../../prompt");
const dialogs = require("../../apphosting/secrets/dialogs");
const config = require("../../apphosting/config");
const projects_1 = require("../../management/projects");
function toMulti(accounts) {
const m = {
buildServiceAccounts: [accounts.buildServiceAccount],
runServiceAccounts: [],
};
if (accounts.buildServiceAccount !== accounts.runServiceAccount) {
m.runServiceAccounts.push(accounts.runServiceAccount);
}
return m;
}
async function serviceAccountsForBackend(projectNumber, backend) {
if (backend.serviceAccount) {
return {
buildServiceAccount: backend.serviceAccount,
runServiceAccount: backend.serviceAccount,
};
}
return {
buildServiceAccount: gcb.getDefaultServiceAccount(projectNumber),
runServiceAccount: await gce.getDefaultServiceAccount(projectNumber),
};
}
async function grantSecretAccess(projectId, projectNumber, secretName, accounts) {
const p4saEmail = apphosting.serviceAgentEmail(projectNumber);
const newBindings = [
{
role: "roles/secretmanager.secretAccessor",
members: [...accounts.buildServiceAccounts, ...accounts.runServiceAccounts].map((sa) => `serviceAccount:${sa}`),
},
{
role: "roles/secretmanager.viewer",
members: accounts.buildServiceAccounts.map((sa) => `serviceAccount:${sa}`),
},
{
role: "roles/secretmanager.secretVersionManager",
members: [`serviceAccount:${p4saEmail}`],
},
];
let existingBindings;
try {
existingBindings = (await gcsm.getIamPolicy({ projectId, name: secretName })).bindings || [];
}
catch (err) {
throw new error_1.FirebaseError(`Failed to get IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, { original: (0, error_1.getError)(err) });
}
const updatedBindings = existingBindings.concat(newBindings);
try {
await gcsm.setIamPolicy({ projectId, name: secretName }, updatedBindings);
}
catch (err) {
throw new error_1.FirebaseError(`Failed to set IAM bindings ${JSON.stringify(newBindings)} on secret: ${secretName}. Ensure you have the permissions to do so and try again. ` +
"For more information visit https://cloud.google.com/secret-manager/docs/manage-access-to-secrets#required-roles", { original: (0, error_1.getError)(err) });
}
utils.logSuccess(`Successfully set IAM bindings on secret ${secretName}.\n`);
}
async function grantEmailsSecretAccess(projectId, secretNames, emails) {
const typeGuesses = Object.fromEntries(emails.map((email) => [email, "user"]));
for (const secretName of secretNames) {
let existingBindings;
try {
existingBindings = (await gcsm.getIamPolicy({ projectId, name: secretName })).bindings || [];
}
catch (err) {
throw new error_1.FirebaseError(`Failed to get IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again. ` +
"For more information visit https://cloud.google.com/secret-manager/docs/manage-access-to-secrets#required-roles", { original: (0, error_1.getError)(err) });
}
do {
try {
const newBindings = [
{
role: "roles/secretmanager.secretAccessor",
members: Object.entries(typeGuesses).map(([email, type]) => `${type}:${email}`),
},
];
const updatedBindings = existingBindings.concat(newBindings);
await gcsm.setIamPolicy({ projectId, name: secretName }, updatedBindings);
break;
}
catch (err) {
if (!(err instanceof error_1.FirebaseError)) {
throw new error_1.FirebaseError(`Unexpected error updating IAM bindings on secret: ${secretName}`, {
original: (0, error_1.getError)(err),
});
}
const match = /Principal (.*) is of type "([^"]+)"/.exec(err.message);
if (!match) {
throw new error_1.FirebaseError(`Failed to set IAM bindings on secret: ${secretName}. Ensure you have the permissions to do so and try again.`, { original: (0, error_1.getError)(err) });
}
typeGuesses[match[1]] = match[2];
continue;
}
} while (true);
utils.logSuccess(`Successfully set IAM bindings on secret ${secretName}.\n`);
}
}
async function upsertSecret(project, secret, location) {
let existing;
try {
existing = await gcsm.getSecret(project, secret);
}
catch (err) {
if ((0, error_1.getErrStatus)(err) !== 404) {
throw new error_1.FirebaseError("Unexpected error loading secret", { original: (0, error_1.getError)(err) });
}
await gcsm.createSecret(project, secret, gcsm.labels("apphosting"), location);
return true;
}
const replication = existing.replication?.userManaged;
if (location &&
(replication?.replicas?.length !== 1 || replication?.replicas?.[0]?.location !== location)) {
utils.logLabeledError("apphosting", "Secret replication policies cannot be changed after creation");
return null;
}
if ((0, secretManager_2.isFunctionsManaged)(existing)) {
utils.logLabeledWarning("apphosting", `Cloud Functions for Firebase currently manages versions of ${secret}. Continuing will disable ` +
"automatic deletion of old versions.");
const stopTracking = await prompt.confirm({
message: "Do you wish to continue?",
default: false,
});
if (!stopTracking) {
return null;
}
delete existing.labels[secretManager_1.FIREBASE_MANAGED];
await gcsm.patchSecret(project, secret, existing.labels);
}
return false;
}
async function fetchSecrets(projectId, secrets) {
let secretsKeyValuePairs;
try {
const secretPromises = secrets.map(async (secretConfig) => {
const [name, version] = getSecretNameParts(secretConfig.secret);
const value = await gcsm.accessSecretVersion(projectId, name, version);
return [secretConfig.variable, value];
});
const secretEntries = await Promise.all(secretPromises);
secretsKeyValuePairs = new Map(secretEntries);
}
catch (e) {
throw new error_1.FirebaseError(`Error exporting secrets`, {
original: e,
});
}
return secretsKeyValuePairs;
}
function getSecretNameParts(secret) {
let [name, version] = secret.split("@");
if (!version) {
version = "latest";
}
return [name, version];
}
async function apphostingSecretsSetAction(secretName, projectId, projectNumber, location, dataFile, nonInteractive) {
if (!projectNumber) {
projectNumber = (await (0, projects_1.getProject)(projectId)).projectNumber;
}
const created = await upsertSecret(projectId, secretName, location);
if (created === null) {
return;
}
else if (created) {
utils.logSuccess(`Created new secret projects/${projectId}/secrets/${secretName}`);
}
const secretValue = await utils.readSecretValue(`Enter a value for ${secretName}`, dataFile);
const version = await gcsm.addVersion(projectId, secretName, secretValue);
utils.logSuccess(`Created new secret version ${gcsm.toSecretVersionResourceName(version)}`);
utils.logBullet(`You can access the contents of the secret's latest value with ${clc.bold(`firebase apphosting:secrets:access ${secretName}\n`)}`);
if (!created) {
return;
}
const type = await prompt.select({
message: "Is this secret for production or only local testing?",
choices: [
{ name: "Production", value: "production" },
{ name: "Local testing only", value: "local" },
],
nonInteractive: !!nonInteractive,
default: "production",
});
if (type === "local") {
const emailList = await prompt.input({
message: "Please enter a comma separated list of user or groups who should have access to this secret:",
});
if (emailList.length) {
await grantEmailsSecretAccess(projectId, [secretName], emailList.split(","));
}
else {
utils.logBullet("To grant access in the future run " +
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --emails [email list]`));
}
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_EMULATORS_YAML_FILE);
return;
}
const accounts = await dialogs.selectBackendServiceAccounts(projectNumber, projectId, !!nonInteractive);
if (!accounts.buildServiceAccounts.length && !accounts.runServiceAccounts.length) {
utils.logWarning(`To use this secret in your backend, you must grant access. You can do so in the future with ${clc.bold("firebase apphosting:secrets:grantaccess")}`);
}
else {
await grantSecretAccess(projectId, projectNumber, secretName, accounts);
}
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_BASE_YAML_FILE);
utils.logBullet("To grant additional users access to this secret run " +
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --email [email list]`) +
".\nTo grant additional backends access to this secret run " +
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --backend [backend ID]`));
}