firebase-tools 13.33.0 → 13.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,8 +9,8 @@ const fileUtils_1 = require("../dataconnect/fileUtils");
9
9
  const schemaMigration_1 = require("../dataconnect/schemaMigration");
10
10
  const requireAuth_1 = require("../requireAuth");
11
11
  const error_1 = require("../error");
12
- const permissions_1 = require("../gcp/cloudsql/permissions");
13
- const allowedRoles = Object.keys(permissions_1.fdcSqlRoleMap);
12
+ const permissions_setup_1 = require("../gcp/cloudsql/permissions_setup");
13
+ const allowedRoles = Object.keys(permissions_setup_1.fdcSqlRoleMap);
14
14
  exports.command = new command_1.Command("dataconnect:sql:grant [serviceId]")
15
15
  .description("Grants the SQL role <role> to the provided user or service account <email>.")
16
16
  .option("-R, --role <role>", "The SQL role to grant. One of: owner, writer, or reader.")
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const projectUtils_1 = require("../projectUtils");
6
+ const fileUtils_1 = require("../dataconnect/fileUtils");
7
+ const error_1 = require("../error");
8
+ const requireAuth_1 = require("../requireAuth");
9
+ const requirePermissions_1 = require("../requirePermissions");
10
+ const ensureApis_1 = require("../dataconnect/ensureApis");
11
+ const permissions_setup_1 = require("../gcp/cloudsql/permissions_setup");
12
+ const permissions_1 = require("../gcp/cloudsql/permissions");
13
+ const schemaMigration_1 = require("../dataconnect/schemaMigration");
14
+ exports.command = new command_1.Command("dataconnect:sql:setup [serviceId]")
15
+ .description("Setup your CloudSQL database")
16
+ .before(requirePermissions_1.requirePermissions, [
17
+ "firebasedataconnect.services.list",
18
+ "firebasedataconnect.schemas.list",
19
+ "firebasedataconnect.schemas.update",
20
+ "cloudsql.instances.connect",
21
+ ])
22
+ .before(requireAuth_1.requireAuth)
23
+ .action(async (serviceId, options) => {
24
+ var _a;
25
+ const projectId = (0, projectUtils_1.needProjectId)(options);
26
+ await (0, ensureApis_1.ensureApis)(projectId);
27
+ const serviceInfo = await (0, fileUtils_1.pickService)(projectId, options.config, serviceId);
28
+ const instanceId = (_a = serviceInfo.dataConnectYaml.schema.datasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql.instanceId;
29
+ if (!instanceId) {
30
+ throw new error_1.FirebaseError("dataconnect.yaml is missing field schema.datasource.postgresql.cloudsql.instanceId");
31
+ }
32
+ const { databaseId } = (0, schemaMigration_1.getIdentifiers)(serviceInfo.schema);
33
+ const schemaInfo = await (0, permissions_setup_1.getSchemaMetadata)(instanceId, databaseId, permissions_1.DEFAULT_SCHEMA, options);
34
+ await (0, permissions_setup_1.setupSQLPermissions)(instanceId, databaseId, schemaInfo, options);
35
+ });
@@ -216,6 +216,7 @@ function load(client) {
216
216
  client.dataconnect.services.list = loadCommand("dataconnect-services-list");
217
217
  client.dataconnect.sql = {};
218
218
  client.dataconnect.sql.diff = loadCommand("dataconnect-sql-diff");
219
+ client.dataconnect.sql.setup = loadCommand("dataconnect-sql-setup");
219
220
  client.dataconnect.sql.migrate = loadCommand("dataconnect-sql-migrate");
220
221
  client.dataconnect.sql.grant = loadCommand("dataconnect-sql-grant");
221
222
  client.dataconnect.sql.shell = loadCommand("dataconnect-sql-shell");
@@ -6,13 +6,15 @@ const sql_formatter_1 = require("sql-formatter");
6
6
  const types_1 = require("./types");
7
7
  const client_1 = require("./client");
8
8
  const connect_1 = require("../gcp/cloudsql/connect");
9
- const permissions_1 = require("../gcp/cloudsql/permissions");
10
- const cloudSqlAdminClient = require("../gcp/cloudsql/cloudsqladmin");
11
9
  const projectUtils_1 = require("../projectUtils");
10
+ const permissions_setup_1 = require("../gcp/cloudsql/permissions_setup");
11
+ const permissions_1 = require("../gcp/cloudsql/permissions");
12
12
  const prompt_1 = require("../prompt");
13
13
  const logger_1 = require("../logger");
14
14
  const error_1 = require("../error");
15
15
  const utils_1 = require("../utils");
16
+ const cloudsqladmin_1 = require("../gcp/cloudsql/cloudsqladmin");
17
+ const cloudSqlAdminClient = require("../gcp/cloudsql/cloudsqladmin");
16
18
  const errors = require("./errors");
17
19
  async function diffSchema(schema, schemaValidation) {
18
20
  const { serviceName, instanceName, databaseId } = getIdentifiers(schema);
@@ -156,12 +158,29 @@ async function grantRoleToUserInSchema(options, schema) {
156
158
  const { instanceId, databaseId } = getIdentifiers(schema);
157
159
  const projectId = (0, projectUtils_1.needProjectId)(options);
158
160
  const { user, mode } = (0, connect_1.toDatabaseUser)(email);
159
- const fdcSqlRole = permissions_1.fdcSqlRoleMap[role](databaseId);
160
- const userIsCSQLAdmin = await (0, permissions_1.iamUserIsCSQLAdmin)(options);
161
+ const fdcSqlRole = permissions_setup_1.fdcSqlRoleMap[role](databaseId);
162
+ const userIsCSQLAdmin = await (0, cloudsqladmin_1.iamUserIsCSQLAdmin)(options);
161
163
  if (!userIsCSQLAdmin) {
162
164
  throw new error_1.FirebaseError(`Only users with 'roles/cloudsql.admin' can grant SQL roles. If you do not have this role, ask your database administrator to run this command or manually grant ${fdcSqlRole} to ${user}`);
163
165
  }
164
- await (0, connect_1.setupIAMUsers)(instanceId, databaseId, options);
166
+ const schemaInfo = await (0, permissions_setup_1.getSchemaMetadata)(instanceId, databaseId, permissions_1.DEFAULT_SCHEMA, options);
167
+ let isGreenfieldSetup = schemaInfo.setupStatus === permissions_setup_1.SchemaSetupStatus.GreenField;
168
+ switch (schemaInfo.setupStatus) {
169
+ case permissions_setup_1.SchemaSetupStatus.NotSetup:
170
+ case permissions_setup_1.SchemaSetupStatus.NotFound:
171
+ const newSetupStatus = await (0, permissions_setup_1.setupSQLPermissions)(instanceId, databaseId, schemaInfo, options);
172
+ isGreenfieldSetup = newSetupStatus === permissions_setup_1.SchemaSetupStatus.GreenField;
173
+ break;
174
+ default:
175
+ logger_1.logger.info(`Detected schema "${schemaInfo.name}" is setup in ${schemaInfo.setupStatus} mode. Skipping Setup.`);
176
+ break;
177
+ }
178
+ if (!isGreenfieldSetup && fdcSqlRole === (0, permissions_1.firebaseowner)(databaseId, permissions_1.DEFAULT_SCHEMA)) {
179
+ const newSetupStatus = await (0, permissions_setup_1.setupSQLPermissions)(instanceId, databaseId, schemaInfo, options);
180
+ if (newSetupStatus !== permissions_setup_1.SchemaSetupStatus.GreenField) {
181
+ throw new error_1.FirebaseError(`Can't grant owner rule for brownfield databases. Consider fully migrating your database to FDC using 'firebase dataconnect:sql:setup'`);
182
+ }
183
+ }
165
184
  await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user);
166
185
  await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, [`GRANT "${fdcSqlRole}" TO "${user}"`], false);
167
186
  }
@@ -232,16 +251,20 @@ async function handleIncompatibleSchemaError(args) {
232
251
  if (commandsToExecute.length) {
233
252
  const commandsToExecuteBySuperUser = commandsToExecute.filter((sql) => sql.startsWith("CREATE EXTENSION") || sql.startsWith("CREATE SCHEMA"));
234
253
  const commandsToExecuteByOwner = commandsToExecute.filter((sql) => !commandsToExecuteBySuperUser.includes(sql));
235
- const userIsCSQLAdmin = await (0, permissions_1.iamUserIsCSQLAdmin)(options);
254
+ const userIsCSQLAdmin = await (0, cloudsqladmin_1.iamUserIsCSQLAdmin)(options);
236
255
  if (!userIsCSQLAdmin && commandsToExecuteBySuperUser.length) {
237
256
  throw new error_1.FirebaseError(`Some SQL commands required for this migration require Admin permissions.\n
238
257
  Please ask a user with 'roles/cloudsql.admin' to apply the following commands.\n
239
258
  ${commandsToExecuteBySuperUser.join("\n")}`);
240
259
  }
241
- if (userIsCSQLAdmin) {
242
- await (0, connect_1.setupIAMUsers)(instanceId, databaseId, options);
260
+ const schemaInfo = await (0, permissions_setup_1.getSchemaMetadata)(instanceId, databaseId, permissions_1.DEFAULT_SCHEMA, options);
261
+ if (schemaInfo.setupStatus !== permissions_setup_1.SchemaSetupStatus.GreenField) {
262
+ const newSetupStatus = await (0, permissions_setup_1.setupSQLPermissions)(instanceId, databaseId, schemaInfo, options, true);
263
+ if (newSetupStatus !== permissions_setup_1.SchemaSetupStatus.GreenField) {
264
+ throw new error_1.FirebaseError(`Can't migrate brownfield databases. Consider fully migrating your database to FDC using 'firebase dataconnect:sql:setup'`);
265
+ }
243
266
  }
244
- if (!(await (0, permissions_1.checkSQLRoleIsGranted)(options, instanceId, databaseId, (0, permissions_1.firebaseowner)(databaseId), (await (0, connect_1.getIAMUser)(options)).user))) {
267
+ if (!(await (0, permissions_setup_1.checkSQLRoleIsGranted)(options, instanceId, databaseId, (0, permissions_1.firebaseowner)(databaseId), (await (0, connect_1.getIAMUser)(options)).user))) {
245
268
  throw new error_1.FirebaseError(`Command aborted. Only users granted firebaseowner SQL role can run migrations.`);
246
269
  }
247
270
  if (commandsToExecuteBySuperUser.length) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.deploy = exports.prepareFrameworksIfNeeded = exports.matchesHostingTarget = void 0;
3
+ exports.deploy = exports.isDeployingWebFramework = void 0;
4
4
  const clc = require("colorette");
5
5
  const logger_1 = require("../logger");
6
6
  const api_1 = require("../api");
@@ -40,25 +40,20 @@ const chain = async function (fns, context, options, payload) {
40
40
  await latest(context, options, payload);
41
41
  }
42
42
  };
43
- const matchesHostingTarget = (only, target) => {
44
- if (!only)
45
- return true;
46
- if (!only.includes("hosting:"))
47
- return true;
48
- const targetStr = `hosting:${target !== null && target !== void 0 ? target : ""}`;
49
- return only.split(",").some((t) => t === targetStr);
50
- };
51
- exports.matchesHostingTarget = matchesHostingTarget;
52
- const prepareFrameworksIfNeeded = async function (targetNames, options, context) {
43
+ const isDeployingWebFramework = (options) => {
53
44
  const config = options.config.get("hosting");
54
- if (Array.isArray(config)
55
- ? config.some((it) => it.source && (0, exports.matchesHostingTarget)(options.only, it.target))
56
- : config.source) {
57
- experiments.assertEnabled("webframeworks", "deploy a web framework from source");
58
- await (0, frameworks_1.prepareFrameworks)("deploy", targetNames, context, options);
59
- }
45
+ const webFrameworkInConfig = (Array.isArray(config) ? config : [config]).find((it) => it.source);
46
+ if (!webFrameworkInConfig)
47
+ return false;
48
+ if (!options.only)
49
+ return true;
50
+ return options.only.split(",").some((it) => {
51
+ const [target, site] = it.split(":");
52
+ return (target === "hosting" &&
53
+ [webFrameworkInConfig.site, webFrameworkInConfig.target].includes(site));
54
+ });
60
55
  };
61
- exports.prepareFrameworksIfNeeded = prepareFrameworksIfNeeded;
56
+ exports.isDeployingWebFramework = isDeployingWebFramework;
62
57
  const deploy = async function (targetNames, options, customContext = {}) {
63
58
  var _a, _b, _c;
64
59
  const projectId = (0, projectUtils_1.needProjectId)(options);
@@ -70,8 +65,9 @@ const deploy = async function (targetNames, options, customContext = {}) {
70
65
  const releases = [];
71
66
  const postdeploys = [];
72
67
  const startTime = Date.now();
73
- if (targetNames.includes("hosting")) {
74
- await (0, exports.prepareFrameworksIfNeeded)(targetNames, options, context);
68
+ if (targetNames.includes("hosting") && (0, exports.isDeployingWebFramework)(options)) {
69
+ experiments.assertEnabled("webframeworks", "deploy a web framework from source");
70
+ await (0, frameworks_1.prepareFrameworks)("deploy", targetNames, context, options);
75
71
  }
76
72
  if (targetNames.includes("hosting") && (0, prepare_1.hasPinnedFunctions)(options)) {
77
73
  experiments.assertEnabled("pintags", "deploy a tagged function as a hosting rewrite");
@@ -48,20 +48,20 @@ const EMULATOR_UPDATE_DETAILS = {
48
48
  },
49
49
  dataconnect: process.platform === "darwin"
50
50
  ? {
51
- version: "1.8.4",
52
- expectedSize: 25588480,
53
- expectedChecksum: "421f6226a0433b824642c03eb0b6862d",
51
+ version: "1.8.5",
52
+ expectedSize: 25600768,
53
+ expectedChecksum: "7e2a935f972ce30e075cca1f36e24663",
54
54
  }
55
55
  : process.platform === "win32"
56
56
  ? {
57
- version: "1.8.4",
58
- expectedSize: 26020352,
59
- expectedChecksum: "b78c95a7f071f127acda3a76cdcc8c48",
57
+ version: "1.8.5",
58
+ expectedSize: 26031616,
59
+ expectedChecksum: "da063f9893b0ff4c99f280653c717977",
60
60
  }
61
61
  : {
62
- version: "1.8.4",
63
- expectedSize: 25501848,
64
- expectedChecksum: "0aabd622b1a99b2a0d9c9dec4c7404fc",
62
+ version: "1.8.5",
63
+ expectedSize: 25514136,
64
+ expectedChecksum: "6564f779f7f5a467e587d7093ed7c3e3",
65
65
  },
66
66
  };
67
67
  exports.DownloadDetails = {
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.listUsers = exports.deleteUser = exports.getUser = exports.createUser = exports.deleteDatabase = exports.createDatabase = exports.getDatabase = exports.listDatabases = exports.updateInstanceForDataConnect = exports.createInstance = exports.instanceConsoleLink = exports.getInstance = exports.listInstances = void 0;
3
+ exports.listUsers = exports.deleteUser = exports.getUser = exports.createUser = exports.deleteDatabase = exports.createDatabase = exports.getDatabase = exports.listDatabases = exports.updateInstanceForDataConnect = exports.createInstance = exports.instanceConsoleLink = exports.getInstance = exports.listInstances = exports.iamUserIsCSQLAdmin = void 0;
4
4
  const apiv2_1 = require("../../apiv2");
5
5
  const api_1 = require("../../api");
6
6
  const operationPoller = require("../../operation-poller");
7
+ const projectUtils_1 = require("../../projectUtils");
8
+ const logger_1 = require("../../logger");
9
+ const iam_1 = require("../iam");
7
10
  const error_1 = require("../../error");
8
11
  const API_VERSION = "v1";
9
12
  const client = new apiv2_1.Client({
@@ -11,6 +14,24 @@ const client = new apiv2_1.Client({
11
14
  auth: true,
12
15
  apiVersion: API_VERSION,
13
16
  });
17
+ async function iamUserIsCSQLAdmin(options) {
18
+ const projectId = (0, projectUtils_1.needProjectId)(options);
19
+ const requiredPermissions = [
20
+ "cloudsql.instances.connect",
21
+ "cloudsql.instances.get",
22
+ "cloudsql.users.create",
23
+ "cloudsql.users.update",
24
+ ];
25
+ try {
26
+ const iamResult = await (0, iam_1.testIamPermissions)(projectId, requiredPermissions);
27
+ return iamResult.passed;
28
+ }
29
+ catch (err) {
30
+ logger_1.logger.debug(`[iam] error while checking permissions, command may fail: ${err}`);
31
+ return false;
32
+ }
33
+ }
34
+ exports.iamUserIsCSQLAdmin = iamUserIsCSQLAdmin;
14
35
  async function listInstances(projectId) {
15
36
  var _a;
16
37
  const res = await client.get(`projects/${projectId}/instances`);
@@ -11,7 +11,6 @@ const utils = require("../../utils");
11
11
  const logger_1 = require("../../logger");
12
12
  const error_1 = require("../../error");
13
13
  const fbToolsAuthClient_1 = require("./fbToolsAuthClient");
14
- const permissions_1 = require("./permissions");
15
14
  async function execute(sqlStatements, opts) {
16
15
  const logFn = opts.silent ? logger_1.logger.debug : logger_1.logger.info;
17
16
  const instance = await cloudSqlAdminClient.getInstance(opts.projectId, opts.instanceId);
@@ -61,11 +60,12 @@ async function execute(sqlStatements, opts) {
61
60
  }
62
61
  }
63
62
  const conn = await pool.connect();
63
+ const results = [];
64
64
  logFn(`Logged in as ${opts.username}`);
65
65
  for (const s of sqlStatements) {
66
66
  logFn(`Executing: '${s}'`);
67
67
  try {
68
- await conn.query(s);
68
+ results.push(await conn.query(s));
69
69
  }
70
70
  catch (err) {
71
71
  throw new error_1.FirebaseError(`Error executing ${err}`);
@@ -74,6 +74,7 @@ async function execute(sqlStatements, opts) {
74
74
  conn.release();
75
75
  await pool.end();
76
76
  connector.close();
77
+ return results;
77
78
  }
78
79
  exports.execute = execute;
79
80
  async function executeSqlCmdsAsIamUser(options, instanceId, databaseId, cmds, silent = false) {
@@ -93,7 +94,7 @@ async function executeSqlCmdsAsSuperUser(options, instanceId, databaseId, cmds,
93
94
  const superuser = "firebasesuperuser";
94
95
  const temporaryPassword = utils.generateId(20);
95
96
  await cloudSqlAdminClient.createUser(projectId, instanceId, "BUILT_IN", superuser, temporaryPassword);
96
- return await execute([`SET ROLE = cloudsqlsuperuser`, ...cmds], {
97
+ return await execute([`SET ROLE = '${superuser}'`, ...cmds], {
97
98
  projectId,
98
99
  instanceId,
99
100
  databaseId,
@@ -122,12 +123,6 @@ async function setupIAMUsers(instanceId, databaseId, options) {
122
123
  const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
123
124
  const { user: fdcP4SAUser, mode: fdcP4SAmode } = toDatabaseUser(getDataConnectP4SA(projectNumber));
124
125
  await cloudSqlAdminClient.createUser(projectId, instanceId, fdcP4SAmode, fdcP4SAUser);
125
- await (0, permissions_1.setupSQLPermissions)(instanceId, databaseId, options, true);
126
- const grants = [
127
- `GRANT "${(0, permissions_1.firebaseowner)(databaseId)}" TO "${user}"`,
128
- `GRANT "${(0, permissions_1.firebasewriter)(databaseId)}" TO "${fdcP4SAUser}"`,
129
- ];
130
- await executeSqlCmdsAsSuperUser(options, instanceId, databaseId, grants, true);
131
126
  return user;
132
127
  }
133
128
  exports.setupIAMUsers = setupIAMUsers;
@@ -1,89 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupSQLPermissions = exports.iamUserIsCSQLAdmin = exports.checkSQLRoleIsGranted = exports.fdcSqlRoleMap = exports.firebasewriter = exports.firebasereader = exports.firebaseowner = void 0;
4
- const projectUtils_1 = require("../../projectUtils");
5
- const connect_1 = require("./connect");
6
- const iam_1 = require("../iam");
7
- const logger_1 = require("../../logger");
8
- const lodash_1 = require("lodash");
9
- const error_1 = require("../../error");
10
- function firebaseowner(databaseId) {
11
- return `firebaseowner_${databaseId}_public`;
3
+ exports.defaultPermissions = exports.readerRolePermissions = exports.writerRolePermissions = exports.ownerRolePermissions = exports.firebasewriter = exports.firebasereader = exports.firebaseowner = exports.FIREBASE_SUPER_USER = exports.DEFAULT_SCHEMA = void 0;
4
+ exports.DEFAULT_SCHEMA = "public";
5
+ exports.FIREBASE_SUPER_USER = "firebasesuperuser";
6
+ function firebaseowner(databaseId, schema = exports.DEFAULT_SCHEMA) {
7
+ return `firebaseowner_${databaseId}_${schema}`;
12
8
  }
13
9
  exports.firebaseowner = firebaseowner;
14
- function firebasereader(databaseId) {
15
- return `firebasereader_${databaseId}_public`;
10
+ function firebasereader(databaseId, schema = exports.DEFAULT_SCHEMA) {
11
+ return `firebasereader_${databaseId}_${schema}`;
16
12
  }
17
13
  exports.firebasereader = firebasereader;
18
- function firebasewriter(databaseId) {
19
- return `firebasewriter_${databaseId}_public`;
14
+ function firebasewriter(databaseId, schema = exports.DEFAULT_SCHEMA) {
15
+ return `firebasewriter_${databaseId}_${schema}`;
20
16
  }
21
17
  exports.firebasewriter = firebasewriter;
22
- exports.fdcSqlRoleMap = {
23
- owner: firebaseowner,
24
- writer: firebasewriter,
25
- reader: firebasereader,
26
- };
27
- async function checkSQLRoleIsGranted(options, instanceId, databaseId, grantedRole, granteeRole) {
28
- const checkCmd = `
29
- DO $$
30
- DECLARE
31
- role_count INTEGER;
32
- BEGIN
33
- -- Count the number of rows matching the criteria
34
- SELECT COUNT(*)
35
- INTO role_count
36
- FROM
37
- pg_auth_members m
38
- JOIN
39
- pg_roles grantee ON grantee.oid = m.member
40
- JOIN
41
- pg_roles granted ON granted.oid = m.roleid
42
- JOIN
43
- pg_roles grantor ON grantor.oid = m.grantor
44
- WHERE
45
- granted.rolname = '${grantedRole}'
46
- AND grantee.rolname = '${granteeRole}';
47
-
48
- -- If no rows were found, raise an exception
49
- IF role_count = 0 THEN
50
- RAISE EXCEPTION 'Role "%", is not granted to role "%".', '${grantedRole}', '${granteeRole}';
51
- END IF;
52
- END $$;
53
- `;
54
- try {
55
- await (0, connect_1.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [checkCmd], true);
56
- return true;
57
- }
58
- catch (e) {
59
- if (e instanceof error_1.FirebaseError && e.message.includes("not granted to role")) {
60
- return false;
61
- }
62
- logger_1.logger.error(`Role Check Failed: ${e}`);
63
- throw e;
64
- }
65
- }
66
- exports.checkSQLRoleIsGranted = checkSQLRoleIsGranted;
67
- async function iamUserIsCSQLAdmin(options) {
68
- const projectId = (0, projectUtils_1.needProjectId)(options);
69
- const requiredPermissions = [
70
- "cloudsql.instances.connect",
71
- "cloudsql.instances.get",
72
- "cloudsql.users.create",
73
- "cloudsql.users.update",
74
- ];
75
- try {
76
- const iamResult = await (0, iam_1.testIamPermissions)(projectId, requiredPermissions);
77
- return iamResult.passed;
78
- }
79
- catch (err) {
80
- logger_1.logger.debug(`[iam] error while checking permissions, command may fail: ${err}`);
81
- return false;
82
- }
83
- }
84
- exports.iamUserIsCSQLAdmin = iamUserIsCSQLAdmin;
85
18
  function ownerRolePermissions(databaseId, superuser, schema) {
86
- const firebaseOwnerRole = firebaseowner(databaseId);
19
+ const firebaseOwnerRole = firebaseowner(databaseId, schema);
87
20
  return [
88
21
  `do
89
22
  $$
@@ -102,8 +35,9 @@ function ownerRolePermissions(databaseId, superuser, schema) {
102
35
  `GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA "${schema}" TO "${firebaseOwnerRole}"`,
103
36
  ];
104
37
  }
38
+ exports.ownerRolePermissions = ownerRolePermissions;
105
39
  function writerRolePermissions(databaseId, superuser, schema) {
106
- const firebaseWriterRole = firebasewriter(databaseId);
40
+ const firebaseWriterRole = firebasewriter(databaseId, schema);
107
41
  return [
108
42
  `do
109
43
  $$
@@ -120,15 +54,11 @@ function writerRolePermissions(databaseId, superuser, schema) {
120
54
  `GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA "${schema}" TO "${firebaseWriterRole}"`,
121
55
  `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "${schema}" TO "${firebaseWriterRole}"`,
122
56
  `GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA "${schema}" TO "${firebaseWriterRole}"`,
123
- `SET ROLE = '${firebaseowner(databaseId)}';`,
124
- `ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON TABLES TO "${firebaseWriterRole}";`,
125
- `ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT USAGE ON SEQUENCES TO "${firebaseWriterRole}";`,
126
- `ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT EXECUTE ON FUNCTIONS TO "${firebaseWriterRole}"`,
127
- `SET ROLE = cloudsqlsuperuser`,
128
57
  ];
129
58
  }
59
+ exports.writerRolePermissions = writerRolePermissions;
130
60
  function readerRolePermissions(databaseId, superuser, schema) {
131
- const firebaseReaderRole = firebasereader(databaseId);
61
+ const firebaseReaderRole = firebasereader(databaseId, schema);
132
62
  return [
133
63
  `do
134
64
  $$
@@ -145,21 +75,37 @@ function readerRolePermissions(databaseId, superuser, schema) {
145
75
  `GRANT SELECT ON ALL TABLES IN SCHEMA "${schema}" TO "${firebaseReaderRole}"`,
146
76
  `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "${schema}" TO "${firebaseReaderRole}"`,
147
77
  `GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA "${schema}" TO "${firebaseReaderRole}"`,
148
- `SET ROLE = '${firebaseowner(databaseId)}';`,
149
- `ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT SELECT ON TABLES TO "${firebaseReaderRole}";`,
150
- `ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT SELECT, USAGE ON SEQUENCES TO "${firebaseReaderRole}";`,
151
- `ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT EXECUTE ON FUNCTIONS TO "${firebaseReaderRole}"`,
152
- `SET ROLE = cloudsqlsuperuser`,
153
78
  ];
154
79
  }
155
- async function setupSQLPermissions(instanceId, databaseId, options, silent = false) {
156
- const superuser = "firebasesuperuser";
157
- const revokes = [];
158
- if (await checkSQLRoleIsGranted(options, instanceId, databaseId, "cloudsqlsuperuser", firebaseowner(databaseId))) {
159
- logger_1.logger.warn("Detected cloudsqlsuperuser was previously given to firebase owner, revoking to improve database security.");
160
- revokes.push(`REVOKE "cloudsqlsuperuser" FROM "${firebaseowner(databaseId)}"`);
161
- }
162
- const sqlRoleSetupCmds = (0, lodash_1.concat)(revokes, [`CREATE SCHEMA IF NOT EXISTS "public"`], ownerRolePermissions(databaseId, superuser, "public"), writerRolePermissions(databaseId, superuser, "public"), readerRolePermissions(databaseId, superuser, "public"));
163
- return (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, sqlRoleSetupCmds, silent);
80
+ exports.readerRolePermissions = readerRolePermissions;
81
+ function defaultPermissions(databaseId, schema, ownerRole) {
82
+ const firebaseWriterRole = firebasewriter(databaseId, schema);
83
+ const firebaseReaderRole = firebasereader(databaseId, schema);
84
+ return [
85
+ `ALTER DEFAULT PRIVILEGES
86
+ FOR ROLE "${ownerRole}"
87
+ IN SCHEMA "${schema}"
88
+ GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON TABLES TO "${firebaseWriterRole}";`,
89
+ `ALTER DEFAULT PRIVILEGES
90
+ FOR ROLE "${ownerRole}"
91
+ IN SCHEMA "${schema}"
92
+ GRANT USAGE ON SEQUENCES TO "${firebaseWriterRole}";`,
93
+ `ALTER DEFAULT PRIVILEGES
94
+ FOR ROLE "${ownerRole}"
95
+ IN SCHEMA "${schema}"
96
+ GRANT EXECUTE ON FUNCTIONS TO "${firebaseWriterRole}";`,
97
+ `ALTER DEFAULT PRIVILEGES
98
+ FOR ROLE "${ownerRole}"
99
+ IN SCHEMA "${schema}"
100
+ GRANT SELECT ON TABLES TO "${firebaseReaderRole}";`,
101
+ `ALTER DEFAULT PRIVILEGES
102
+ FOR ROLE "${ownerRole}"
103
+ IN SCHEMA "${schema}"
104
+ GRANT USAGE ON SEQUENCES TO "${firebaseReaderRole}";`,
105
+ `ALTER DEFAULT PRIVILEGES
106
+ FOR ROLE "${ownerRole}"
107
+ IN SCHEMA "${schema}"
108
+ GRANT EXECUTE ON FUNCTIONS TO "${firebaseReaderRole}";`,
109
+ ];
164
110
  }
165
- exports.setupSQLPermissions = setupSQLPermissions;
111
+ exports.defaultPermissions = defaultPermissions;
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.brownfieldSqlSetup = exports.setupBrownfieldAsGreenfield = exports.getSchemaMetadata = exports.greenFieldSchemaSetup = exports.setupSQLPermissions = exports.checkSQLRoleIsGranted = exports.fdcSqlRoleMap = exports.SchemaSetupStatus = void 0;
4
+ const permissions_1 = require("./permissions");
5
+ const cloudsqladmin_1 = require("./cloudsqladmin");
6
+ const connect_1 = require("./connect");
7
+ const logger_1 = require("../../logger");
8
+ const prompt_1 = require("../../prompt");
9
+ const clc = require("colorette");
10
+ const error_1 = require("../../error");
11
+ const projectUtils_1 = require("../../projectUtils");
12
+ const connect_2 = require("./connect");
13
+ const lodash_1 = require("lodash");
14
+ const connect_3 = require("./connect");
15
+ var SchemaSetupStatus;
16
+ (function (SchemaSetupStatus) {
17
+ SchemaSetupStatus["NotSetup"] = "not-setup";
18
+ SchemaSetupStatus["GreenField"] = "greenfield";
19
+ SchemaSetupStatus["BrownField"] = "brownfield";
20
+ SchemaSetupStatus["NotFound"] = "not-found";
21
+ })(SchemaSetupStatus = exports.SchemaSetupStatus || (exports.SchemaSetupStatus = {}));
22
+ exports.fdcSqlRoleMap = {
23
+ owner: permissions_1.firebaseowner,
24
+ writer: permissions_1.firebasewriter,
25
+ reader: permissions_1.firebasereader,
26
+ };
27
+ async function checkSQLRoleIsGranted(options, instanceId, databaseId, grantedRole, granteeRole) {
28
+ const checkCmd = `
29
+ DO $$
30
+ DECLARE
31
+ role_count INTEGER;
32
+ BEGIN
33
+ -- Count the number of rows matching the criteria
34
+ SELECT COUNT(*)
35
+ INTO role_count
36
+ FROM
37
+ pg_auth_members m
38
+ JOIN
39
+ pg_roles grantee ON grantee.oid = m.member
40
+ JOIN
41
+ pg_roles granted ON granted.oid = m.roleid
42
+ JOIN
43
+ pg_roles grantor ON grantor.oid = m.grantor
44
+ WHERE
45
+ granted.rolname = '${grantedRole}'
46
+ AND grantee.rolname = '${granteeRole}';
47
+
48
+ -- If no rows were found, raise an exception
49
+ IF role_count = 0 THEN
50
+ RAISE EXCEPTION 'Role "%", is not granted to role "%".', '${grantedRole}', '${granteeRole}';
51
+ END IF;
52
+ END $$;
53
+ `;
54
+ try {
55
+ await (0, connect_2.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [checkCmd], true);
56
+ return true;
57
+ }
58
+ catch (e) {
59
+ if (e instanceof error_1.FirebaseError && e.message.includes("not granted to role")) {
60
+ return false;
61
+ }
62
+ logger_1.logger.error(`Role Check Failed: ${e}`);
63
+ throw e;
64
+ }
65
+ }
66
+ exports.checkSQLRoleIsGranted = checkSQLRoleIsGranted;
67
+ async function setupSQLPermissions(instanceId, databaseId, schemaInfo, options, silent = false) {
68
+ const schema = schemaInfo.name;
69
+ logger_1.logger.info(`Attempting to Setup SQL schema "${schema}".`);
70
+ const userIsCSQLAdmin = await (0, cloudsqladmin_1.iamUserIsCSQLAdmin)(options);
71
+ if (!userIsCSQLAdmin) {
72
+ throw new error_1.FirebaseError(`Missing required IAM permission to setup SQL schemas. SQL schema setup requires 'roles/cloudsql.admin' or an equivalent role.`);
73
+ }
74
+ await (0, connect_1.setupIAMUsers)(instanceId, databaseId, options);
75
+ if (schemaInfo.setupStatus === SchemaSetupStatus.GreenField) {
76
+ logger_1.logger.info(`Database ${databaseId} has already been setup. Rerunning setup to repair any missing permissions.`);
77
+ await greenFieldSchemaSetup(instanceId, databaseId, schema, options, silent);
78
+ return SchemaSetupStatus.GreenField;
79
+ }
80
+ else {
81
+ logger_1.logger.info(`Detected schema "${schema}" setup status is ${schemaInfo.setupStatus}.`);
82
+ }
83
+ if (schemaInfo.tables.length === 0) {
84
+ logger_1.logger.info(`Found no tables in schema "${schema}", assuming greenfield project.`);
85
+ await greenFieldSchemaSetup(instanceId, databaseId, schema, options, silent);
86
+ logger_1.logger.info(clc.green("Database setup complete."));
87
+ return SchemaSetupStatus.GreenField;
88
+ }
89
+ if (options.nonInteractive || options.force) {
90
+ throw new error_1.FirebaseError(`Schema "${schema}" isn't set up and can only be set up in interactive mode.`);
91
+ }
92
+ const currentTablesOwners = [...new Set(schemaInfo.tables.map((t) => t.owner))];
93
+ logger_1.logger.info(`We found some existing object owners [${currentTablesOwners.join(", ")}] in your cloudsql "${schema}" schema.`);
94
+ const shouldSetupGreenfield = await (0, prompt_1.confirm)({
95
+ message: clc.yellow("Would you like FDC to handle SQL migrations for you moving forward?\n" +
96
+ `This means we will transfer schema and tables ownership to ${(0, permissions_1.firebaseowner)(databaseId, schema)}\n` +
97
+ "Note: your existing migration tools/roles may lose access."),
98
+ default: false,
99
+ });
100
+ if (shouldSetupGreenfield) {
101
+ await setupBrownfieldAsGreenfield(instanceId, databaseId, schemaInfo, options, silent);
102
+ return SchemaSetupStatus.GreenField;
103
+ }
104
+ else {
105
+ logger_1.logger.info(clc.yellow("Setting up database in brownfield mode.\n" +
106
+ `Note: SQL migrations can't be done through ${clc.bold("firebase dataconnect:sql:migrate")} in this mode.`));
107
+ await brownfieldSqlSetup(instanceId, databaseId, schemaInfo, options, silent);
108
+ logger_1.logger.info(clc.green("Brownfield database setup complete."));
109
+ return SchemaSetupStatus.BrownField;
110
+ }
111
+ }
112
+ exports.setupSQLPermissions = setupSQLPermissions;
113
+ async function greenFieldSchemaSetup(instanceId, databaseId, schema, options, silent = false) {
114
+ const revokes = [];
115
+ if (await checkSQLRoleIsGranted(options, instanceId, databaseId, "cloudsqlsuperuser", (0, permissions_1.firebaseowner)(databaseId))) {
116
+ logger_1.logger.warn("Detected cloudsqlsuperuser was previously given to firebase owner, revoking to improve database security.");
117
+ revokes.push(`REVOKE "cloudsqlsuperuser" FROM "${(0, permissions_1.firebaseowner)(databaseId)}"`);
118
+ }
119
+ const user = (await (0, connect_2.getIAMUser)(options)).user;
120
+ const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
121
+ const { user: fdcP4SAUser } = (0, connect_3.toDatabaseUser)((0, connect_3.getDataConnectP4SA)(projectNumber));
122
+ const sqlRoleSetupCmds = (0, lodash_1.concat)(revokes, [`CREATE SCHEMA IF NOT EXISTS "${schema}"`], (0, permissions_1.ownerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema), (0, permissions_1.writerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema), (0, permissions_1.readerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema), `GRANT "${(0, permissions_1.firebaseowner)(databaseId, schema)}" TO "${user}"`, `GRANT "${(0, permissions_1.firebasewriter)(databaseId, schema)}" TO "${fdcP4SAUser}"`, (0, permissions_1.defaultPermissions)(databaseId, schema, (0, permissions_1.firebaseowner)(databaseId, schema)));
123
+ await (0, connect_2.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, sqlRoleSetupCmds, silent);
124
+ }
125
+ exports.greenFieldSchemaSetup = greenFieldSchemaSetup;
126
+ async function getSchemaMetadata(instanceId, databaseId, schema, options) {
127
+ const checkSchemaExists = await (0, connect_2.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [
128
+ `SELECT pg_get_userbyid(nspowner)
129
+ FROM pg_namespace
130
+ WHERE nspname = '${schema}';`,
131
+ ], true);
132
+ if (!checkSchemaExists[0].rows[0]) {
133
+ return {
134
+ name: schema,
135
+ owner: null,
136
+ setupStatus: SchemaSetupStatus.NotFound,
137
+ tables: [],
138
+ };
139
+ }
140
+ const schemaOwner = checkSchemaExists[0].rows[0].pg_get_userbyid;
141
+ const cmd = `SELECT tablename, tableowner FROM pg_tables WHERE schemaname='${schema}'`;
142
+ const res = await (0, connect_2.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, [cmd], true);
143
+ const tables = res[0].rows.map((row) => {
144
+ return {
145
+ name: row.tablename,
146
+ owner: row.tableowner,
147
+ };
148
+ });
149
+ const checkRoleExists = async (role) => {
150
+ const cmd = [`SELECT to_regrole('"${role}"') IS NOT NULL AS exists;`];
151
+ const result = await (0, connect_2.executeSqlCmdsAsIamUser)(options, instanceId, databaseId, cmd, true);
152
+ return result[0].rows[0].exists;
153
+ };
154
+ let setupStatus;
155
+ if (!(await checkRoleExists((0, permissions_1.firebasewriter)(databaseId, schema)))) {
156
+ setupStatus = SchemaSetupStatus.NotSetup;
157
+ }
158
+ else if (tables.every((table) => table.owner === (0, permissions_1.firebaseowner)(databaseId, schema)) &&
159
+ schemaOwner === (0, permissions_1.firebaseowner)(databaseId, schema)) {
160
+ setupStatus = SchemaSetupStatus.GreenField;
161
+ }
162
+ else {
163
+ setupStatus = SchemaSetupStatus.BrownField;
164
+ }
165
+ return {
166
+ name: schema,
167
+ owner: schemaOwner,
168
+ setupStatus,
169
+ tables: tables,
170
+ };
171
+ }
172
+ exports.getSchemaMetadata = getSchemaMetadata;
173
+ async function setupBrownfieldAsGreenfield(instanceId, databaseId, schemaInfo, options, silent = false) {
174
+ const schema = schemaInfo.name;
175
+ await greenFieldSchemaSetup(instanceId, databaseId, schema, options, silent);
176
+ const firebaseOwnerRole = (0, permissions_1.firebaseowner)(databaseId, schema);
177
+ const nonFirebasetablesOwners = [...new Set(schemaInfo.tables.map((t) => t.owner))].filter((owner) => owner !== firebaseOwnerRole);
178
+ const grantCmds = nonFirebasetablesOwners.map((owner) => `GRANT "${(0, permissions_1.firebasewriter)(databaseId, schema)}" TO "${owner}"`);
179
+ const alterTableCmds = schemaInfo.tables.map((table) => `ALTER TABLE "${schema}"."${table.name}" OWNER TO "${firebaseOwnerRole}";`);
180
+ await (0, connect_2.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, [...grantCmds, ...alterTableCmds], silent);
181
+ }
182
+ exports.setupBrownfieldAsGreenfield = setupBrownfieldAsGreenfield;
183
+ async function brownfieldSqlSetup(instanceId, databaseId, schemaInfo, options, silent = false) {
184
+ const schema = schemaInfo.name;
185
+ const uniqueTablesOwners = [...new Set(schemaInfo.tables.map((t) => t.owner))];
186
+ const grantOwnersToFirebasesuperuser = uniqueTablesOwners.map((owner) => `GRANT ${owner} TO ${permissions_1.FIREBASE_SUPER_USER}`);
187
+ const iamUser = (await (0, connect_2.getIAMUser)(options)).user;
188
+ const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
189
+ const { user: fdcP4SAUser } = (0, connect_3.toDatabaseUser)((0, connect_3.getDataConnectP4SA)(projectNumber));
190
+ const firebaseDefaultPermissions = uniqueTablesOwners.flatMap((owner) => (0, permissions_1.defaultPermissions)(databaseId, schema, owner));
191
+ const brownfieldSetupCmds = [
192
+ ...grantOwnersToFirebasesuperuser,
193
+ ...(0, permissions_1.writerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema),
194
+ ...(0, permissions_1.readerRolePermissions)(databaseId, permissions_1.FIREBASE_SUPER_USER, schema),
195
+ `GRANT "${(0, permissions_1.firebasewriter)(databaseId, schema)}" TO "${iamUser}"`,
196
+ `GRANT "${(0, permissions_1.firebasewriter)(databaseId, schema)}" TO "${fdcP4SAUser}"`,
197
+ ...firebaseDefaultPermissions,
198
+ ];
199
+ await (0, connect_2.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, brownfieldSetupCmds, silent);
200
+ }
201
+ exports.brownfieldSqlSetup = brownfieldSqlSetup;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "13.33.0",
3
+ "version": "13.34.0",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {