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:
parent
3e2f648175
commit
94df249775
9 changed files with 183 additions and 101 deletions
2
resources/sql/patches/033.privtest.sql
Normal file
2
resources/sql/patches/033.privtest.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# This is a no-op, just testing the upgrade_schema.php script.
|
||||||
|
SELECT 1;
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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}");
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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 { }
|
12
src/storage/exception/accessdenied/__init__.php
Normal file
12
src/storage/exception/accessdenied/__init__.php
Normal 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');
|
Loading…
Reference in a new issue