1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-17 18:21:11 +01:00

Improve schema upgrade workflow for unprivileged users

Summary:
In a basically reasonable configuration where you connect
with a non-privileged user from the web workflow, upgrade_schema.php
won't have enough privileges. Allow the user to override the normal
auth with -u and -p.

Test Plan:
Tried to do a schema upgrade with an underprivileged user,
got a useful error message instead of garbage.

Reviewed By: Girish
Reviewers: Girish, davidrecordon, jungejason, tuomaspelkonen, aran
CC: aran, epriestley, Girish
Differential Revision: 191
This commit is contained in:
epriestley 2011-04-30 00:18:13 -07:00
parent 3e2f648175
commit 94df249775
9 changed files with 183 additions and 101 deletions

View file

@ -0,0 +1,2 @@
# This is a no-op, just testing the upgrade_schema.php script.
SELECT 1;

View file

@ -25,14 +25,14 @@ phutil_require_module('phutil', 'console');
define('SCHEMA_VERSION_TABLE_NAME', 'schema_version'); define('SCHEMA_VERSION_TABLE_NAME', 'schema_version');
if (isset($argv[1]) && !is_numeric($argv[1])) { $options = getopt('v:u:p:') + array(
print 'v' => null,
"USAGE: ./update_schema.php [first_patch_version]\n\n". 'u' => null,
"run './update_schema.php 12' to apply all patches starting from ". 'p' => null,
"version 12.\n". );
"run './update_schema.php' to apply all patches that are new since\n".
"the last time this script was run\n\n"; if ($options['v'] && !is_numeric($options['v'])) {
exit(0); usage();
} }
echo phutil_console_wrap( echo phutil_console_wrap(
@ -45,113 +45,141 @@ if (!phutil_console_confirm('Are you ready to continue?')) {
} }
// Use always the version from the commandline if it is defined // Use always the version from the commandline if it is defined
$next_version = isset($argv[1]) ? (int)$argv[1] : null; $next_version = isset($options['v']) ? (int)$options['v'] : null;
// Dummy class needed for creating our database if ($options['u']) {
class DummyUser extends PhabricatorLiskDAO { $conn_user = $options['u'];
public function getApplicationName() { $conn_pass = $options['p'];
return 'user'; } else {
} $conn_user = PhabricatorEnv::getEnvConfig('mysql.user');
$conn_pass = PhabricatorEnv::getEnvConfig('mysql.pass');
} }
$conn_host = PhabricatorEnv::getEnvConfig('mysql.host');
// Class needed for setting up the actual SQL connection $conn = new AphrontMySQLDatabaseConnection(
class PhabricatorSchemaVersion extends PhabricatorLiskDAO { array(
public function getApplicationName() { 'user' => $conn_user,
return 'meta_data'; 'pass' => $conn_pass,
} 'host' => $conn_host,
} 'database' => null,
));
// Connect to 'phabricator_user' db first to create our db try {
$conn = id(new DummyUser())->establishConnection('w');
$create_sql = <<<END $create_sql = <<<END
CREATE DATABASE IF NOT EXISTS `phabricator_meta_data`; CREATE DATABASE IF NOT EXISTS `phabricator_meta_data`;
END; END;
queryfx($conn, $create_sql); queryfx($conn, $create_sql);
// 'phabricator_meta_data' database exists, let's connect to it now $create_sql = <<<END
$conn = id(new PhabricatorSchemaVersion())->establishConnection('w'); CREATE TABLE IF NOT EXISTS phabricator_meta_data.`schema_version` (
$create_sql = <<<END `version` INTEGER not null
CREATE TABLE IF NOT EXISTS phabricator_meta_data.`schema_version` ( );
`version` INTEGER not null
);
END; END;
queryfx($conn, $create_sql); queryfx($conn, $create_sql);
// Get the version only if commandline argument wasn't given // Get the version only if commandline argument wasn't given
if ($next_version === null) { if ($next_version === null) {
$version = queryfx_one( $version = queryfx_one(
$conn, $conn,
'SELECT * FROM %T', 'SELECT * FROM phabricator_meta_data.%T',
SCHEMA_VERSION_TABLE_NAME); SCHEMA_VERSION_TABLE_NAME);
if (!$version) { if (!$version) {
print "*** No version information in the database ***\n"; print "*** No version information in the database ***\n";
print "*** Give the first patch version which to ***\n"; print "*** Give the first patch version which to ***\n";
print "*** apply as the command line argument ***\n"; print "*** apply as the command line argument ***\n";
exit(-1); exit(-1);
}
$next_version = $version['version'] + 1;
} }
$next_version = $version['version'] + 1; // Find the patch files
} $patches_dir = $root.'/resources/sql/patches/';
$finder = id(new FileFinder($patches_dir))
->withSuffix('sql');
$results = $finder->find();
// Find the patch files $patches = array();
$patches_dir = $root.'/resources/sql/patches/'; foreach ($results as $r) {
$finder = id(new FileFinder($patches_dir)) $matches = array();
->withSuffix('sql'); if (preg_match('/(\d+)\..*\.sql$/', $r, $matches)) {
$results = $finder->find(); $patches[] = array('version' => (int)$matches[1],
'file' => $r);
$patches = array(); } else {
foreach ($results as $r) { print
$matches = array(); "*** WARNING : File {$r} does not follow the normal naming ".
if (preg_match('/(\d+)\..*\.sql$/', $r, $matches)) { "convention. ***\n";
$patches[] = array('version' => (int)$matches[1], }
'file' => $r);
} else {
print
"*** WARNING : File {$r} does not follow the normal naming ".
"convention. ***\n";
}
}
// Files are in some 'random' order returned by the operating system
// We need to apply them in proper order
$patches = isort($patches, 'version');
$patch_applied = false;
foreach ($patches as $patch) {
if ($patch['version'] < $next_version) {
continue;
} }
print "Applying patch {$patch['file']}\n"; // Files are in some 'random' order returned by the operating system
// We need to apply them in proper order
$patches = isort($patches, 'version');
$path = Filesystem::resolvePath($patches_dir.$patch['file']); $patch_applied = false;
foreach ($patches as $patch) {
if ($patch['version'] < $next_version) {
continue;
}
$user = PhabricatorEnv::getEnvConfig('mysql.user'); print "Applying patch {$patch['file']}\n";
$pass = PhabricatorEnv::getEnvConfig('mysql.pass');
$host = PhabricatorEnv::getEnvConfig('mysql.host');
list($stdout, $stderr) = execx( $path = Filesystem::resolvePath($patches_dir.$patch['file']);
"mysql --user=%s --password=%s --host=%s < %s",
$user, $pass, $host, $path);
if ($stderr) { list($stdout, $stderr) = execx(
print $stderr; "mysql --user=%s --password=%s --host=%s < %s",
exit(-1); $conn_user,
$conn_pass,
$conn_host,
$path);
if ($stderr) {
print $stderr;
exit(-1);
}
// Patch was successful, update the db with the latest applied patch version
// 'DELETE' and 'INSERT' instead of update, because the table might be empty
queryfx(
$conn,
'DELETE FROM phabricator_meta_data.%T',
SCHEMA_VERSION_TABLE_NAME);
queryfx(
$conn,
'INSERT INTO phabricator_meta_data.%T VALUES (%d)',
SCHEMA_VERSION_TABLE_NAME,
$patch['version']);
$patch_applied = true;
} }
// Patch was successful, update the db with the latest applied patch version if (!$patch_applied) {
// 'DELETE' and 'INSERT' instead of update, because the table might be empty print "Your database is already up-to-date.\n";
queryfx($conn, 'DELETE FROM %T', SCHEMA_VERSION_TABLE_NAME); }
queryfx(
$conn,
'INSERT INTO %T values (%d)',
SCHEMA_VERSION_TABLE_NAME,
$patch['version']);
$patch_applied = true; } catch (AphrontQueryAccessDeniedException $ex) {
echo
"ACCESS DENIED\n".
"The user '{$conn_user}' does not have sufficient MySQL privileges to\n".
"execute the schema upgrade. Use the -u and -p flags to run as a user\n".
"with more privileges (e.g., root).".
"\n\n".
"EXCEPTION:\n".
$ex->getMessage().
"\n\n";
exit(1);
} }
if (!$patch_applied) { function usage() {
print "Your database is already up-to-date.\n"; echo
"usage: upgrade_schema.php [-v version] [-u user -p pass]".
"\n\n".
"Run 'upgrade_schema.php -v 12' to apply all patches starting from ".
"version 12.\n".
"Run 'upgrade_schema.php -u root -p hunter2' to override the configured ".
"default user.\n";
exit(1);
} }

View file

@ -45,6 +45,7 @@ phutil_register_library_map(array(
'AphrontPageView' => 'view/page/base', 'AphrontPageView' => 'view/page/base',
'AphrontPagerView' => 'view/control/pager', 'AphrontPagerView' => 'view/control/pager',
'AphrontPanelView' => 'view/layout/panel', 'AphrontPanelView' => 'view/layout/panel',
'AphrontQueryAccessDeniedException' => 'storage/exception/accessdenied',
'AphrontQueryConnectionException' => 'storage/exception/connection', 'AphrontQueryConnectionException' => 'storage/exception/connection',
'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost', 'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost',
'AphrontQueryCountException' => 'storage/exception/count', 'AphrontQueryCountException' => 'storage/exception/count',
@ -509,6 +510,7 @@ phutil_register_library_map(array(
'AphrontPageView' => 'AphrontView', 'AphrontPageView' => 'AphrontView',
'AphrontPagerView' => 'AphrontView', 'AphrontPagerView' => 'AphrontView',
'AphrontPanelView' => 'AphrontView', 'AphrontPanelView' => 'AphrontView',
'AphrontQueryAccessDeniedException' => 'AphrontQueryRecoverableException',
'AphrontQueryConnectionException' => 'AphrontQueryException', 'AphrontQueryConnectionException' => 'AphrontQueryException',
'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException', 'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException',
'AphrontQueryCountException' => 'AphrontQueryException', 'AphrontQueryCountException' => 'AphrontQueryException',

View file

@ -119,4 +119,5 @@ change by providing overrides in ##myconfig.conf.php##.
= Upgrading Schema = = Upgrading Schema =
After you have configured Phabricator, you need to upgrade the database After you have configured Phabricator, you need to upgrade the database
schema, see @{article:Upgrading Schema} schema, see @{article:Upgrading Schema}. You'll also need to do this after you
update the code in the future.

View file

@ -14,19 +14,25 @@ configured your Phabricator environment. If you haven't, see
If you are doing this for the first time to a freshly installed MySQL database, If you are doing this for the first time to a freshly installed MySQL database,
run the following command: run the following command:
PHABRICATOR_ENV=<your_config> php path/to/phabricator/scripts/sql/upgrade_schema.php 0 PHABRICATOR_ENV=<your_config> path/to/phabricator/scripts/sql/upgrade_schema.php -v 0
This will install all the patches starting from 0. Running this script will This will install all the patches starting from 0. Running this script will
store the information of the latest installed patch in the Phabricator database. store the information of the latest installed patch in the Phabricator database.
Next time you want to upgrade your schema, just run: Next time you want to upgrade your schema, just run:
PHABRICATOR_ENV=<your_config> php path/to/phabricator/scripts/sql/upgrade_schema.php PHABRICATOR_ENV=<your_config> path/to/phabricator/scripts/sql/upgrade_schema.php
This will install all the patches that are new since the last time you ran This will install all the patches that are new since the last time you ran
this script. this script.
If your configuration uses an unprivileged user to connect to the database, you
may have to override the default user so the schema changes can be applied with
root or some other admin user:
PHABRICATOR_ENV=<your_config> path/to/phabricator/scripts/sql/upgrade_schema.php -u <user> -p <pass>
If you need to upgrade the schema starting from a specific patch, just run: If you need to upgrade the schema starting from a specific patch, just run:
PHABRICATOR_ENV=<your_config> php path/to/phabricator/scripts/sql/upgrade_schema.php <patch_number> PHABRICATOR_ENV=<your_config> path/to/phabricator/scripts/sql/upgrade_schema.php -v <patch_number>
However, this isn't usually needed. However, this isn't usually needed.

View file

@ -144,9 +144,11 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
"{$error}."); "{$error}.");
} }
$ret = @mysql_select_db($database, $conn); if ($database !== null) {
if (!$ret) { $ret = @mysql_select_db($database, $conn);
$this->throwQueryException($conn); if (!$ret) {
$this->throwQueryException($conn);
}
} }
$end = microtime(true); $end = microtime(true);
@ -249,6 +251,11 @@ class AphrontMySQLDatabaseConnection extends AphrontDatabaseConnection {
// portable to parse the key out of the error and attach it to the // portable to parse the key out of the error and attach it to the
// exception. // exception.
throw new AphrontQueryDuplicateKeyException("{$errno}: {$error}"); throw new AphrontQueryDuplicateKeyException("{$errno}: {$error}");
case 1044: // Access denied to database
case 1045: // Access denied (auth)
case 1142: // Access denied to table
case 1143: // Access denied to column
throw new AphrontQueryAccessDeniedException("#{$errno}: {$error}");
default: default:
// TODO: 1064 is syntax error, and quite terrible in production. // TODO: 1064 is syntax error, and quite terrible in production.
throw new AphrontQueryException("#{$errno}: {$error}"); throw new AphrontQueryException("#{$errno}: {$error}");

View file

@ -8,6 +8,7 @@
phutil_require_module('phabricator', 'aphront/console/plugin/services/api'); phutil_require_module('phabricator', 'aphront/console/plugin/services/api');
phutil_require_module('phabricator', 'storage/connection/base'); phutil_require_module('phabricator', 'storage/connection/base');
phutil_require_module('phabricator', 'storage/exception/accessdenied');
phutil_require_module('phabricator', 'storage/exception/base'); phutil_require_module('phabricator', 'storage/exception/base');
phutil_require_module('phabricator', 'storage/exception/connection'); phutil_require_module('phabricator', 'storage/exception/connection');
phutil_require_module('phabricator', 'storage/exception/connectionlost'); phutil_require_module('phabricator', 'storage/exception/connectionlost');

View file

@ -0,0 +1,23 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group storage
*/
class AphrontQueryAccessDeniedException
extends AphrontQueryRecoverableException { }

View file

@ -0,0 +1,12 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'storage/exception/recoverable');
phutil_require_source('AphrontQueryAccessDeniedException.php');