Skip to content

Commit 94df249

Browse files
author
epriestley
committedApr 30, 2011
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
1 parent 3e2f648 commit 94df249

9 files changed

+190
-108
lines changed
 
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# This is a no-op, just testing the upgrade_schema.php script.
2+
SELECT 1;

‎scripts/sql/upgrade_schema.php

+129-101
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@
2525

2626
define('SCHEMA_VERSION_TABLE_NAME', 'schema_version');
2727

28-
if (isset($argv[1]) && !is_numeric($argv[1])) {
29-
print
30-
"USAGE: ./update_schema.php [first_patch_version]\n\n".
31-
"run './update_schema.php 12' to apply all patches starting from ".
32-
"version 12.\n".
33-
"run './update_schema.php' to apply all patches that are new since\n".
34-
"the last time this script was run\n\n";
35-
exit(0);
28+
$options = getopt('v:u:p:') + array(
29+
'v' => null,
30+
'u' => null,
31+
'p' => null,
32+
);
33+
34+
if ($options['v'] && !is_numeric($options['v'])) {
35+
usage();
3636
}
3737

3838
echo phutil_console_wrap(
@@ -45,113 +45,141 @@
4545
}
4646

4747
// Use always the version from the commandline if it is defined
48-
$next_version = isset($argv[1]) ? (int)$argv[1] : null;
49-
50-
// Dummy class needed for creating our database
51-
class DummyUser extends PhabricatorLiskDAO {
52-
public function getApplicationName() {
53-
return 'user';
54-
}
48+
$next_version = isset($options['v']) ? (int)$options['v'] : null;
49+
50+
if ($options['u']) {
51+
$conn_user = $options['u'];
52+
$conn_pass = $options['p'];
53+
} else {
54+
$conn_user = PhabricatorEnv::getEnvConfig('mysql.user');
55+
$conn_pass = PhabricatorEnv::getEnvConfig('mysql.pass');
5556
}
57+
$conn_host = PhabricatorEnv::getEnvConfig('mysql.host');
5658

57-
// Class needed for setting up the actual SQL connection
58-
class PhabricatorSchemaVersion extends PhabricatorLiskDAO {
59-
public function getApplicationName() {
60-
return 'meta_data';
61-
}
62-
}
59+
$conn = new AphrontMySQLDatabaseConnection(
60+
array(
61+
'user' => $conn_user,
62+
'pass' => $conn_pass,
63+
'host' => $conn_host,
64+
'database' => null,
65+
));
66+
67+
try {
6368

64-
// Connect to 'phabricator_user' db first to create our db
65-
$conn = id(new DummyUser())->establishConnection('w');
66-
$create_sql = <<<END
67-
CREATE DATABASE IF NOT EXISTS `phabricator_meta_data`;
69+
$create_sql = <<<END
70+
CREATE DATABASE IF NOT EXISTS `phabricator_meta_data`;
6871
END;
69-
queryfx($conn, $create_sql);
72+
queryfx($conn, $create_sql);
7073

71-
// 'phabricator_meta_data' database exists, let's connect to it now
72-
$conn = id(new PhabricatorSchemaVersion())->establishConnection('w');
73-
$create_sql = <<<END
74-
CREATE TABLE IF NOT EXISTS phabricator_meta_data.`schema_version` (
75-
`version` INTEGER not null
76-
);
74+
$create_sql = <<<END
75+
CREATE TABLE IF NOT EXISTS phabricator_meta_data.`schema_version` (
76+
`version` INTEGER not null
77+
);
7778
END;
78-
queryfx($conn, $create_sql);
79-
80-
// Get the version only if commandline argument wasn't given
81-
if ($next_version === null) {
82-
$version = queryfx_one(
83-
$conn,
84-
'SELECT * FROM %T',
85-
SCHEMA_VERSION_TABLE_NAME);
86-
87-
if (!$version) {
88-
print "*** No version information in the database ***\n";
89-
print "*** Give the first patch version which to ***\n";
90-
print "*** apply as the command line argument ***\n";
91-
exit(-1);
79+
queryfx($conn, $create_sql);
80+
81+
// Get the version only if commandline argument wasn't given
82+
if ($next_version === null) {
83+
$version = queryfx_one(
84+
$conn,
85+
'SELECT * FROM phabricator_meta_data.%T',
86+
SCHEMA_VERSION_TABLE_NAME);
87+
88+
if (!$version) {
89+
print "*** No version information in the database ***\n";
90+
print "*** Give the first patch version which to ***\n";
91+
print "*** apply as the command line argument ***\n";
92+
exit(-1);
93+
}
94+
95+
$next_version = $version['version'] + 1;
9296
}
9397

94-
$next_version = $version['version'] + 1;
95-
}
96-
97-
// Find the patch files
98-
$patches_dir = $root.'/resources/sql/patches/';
99-
$finder = id(new FileFinder($patches_dir))
100-
->withSuffix('sql');
101-
$results = $finder->find();
102-
103-
$patches = array();
104-
foreach ($results as $r) {
105-
$matches = array();
106-
if (preg_match('/(\d+)\..*\.sql$/', $r, $matches)) {
107-
$patches[] = array('version' => (int)$matches[1],
108-
'file' => $r);
109-
} else {
110-
print
111-
"*** WARNING : File {$r} does not follow the normal naming ".
112-
"convention. ***\n";
98+
// Find the patch files
99+
$patches_dir = $root.'/resources/sql/patches/';
100+
$finder = id(new FileFinder($patches_dir))
101+
->withSuffix('sql');
102+
$results = $finder->find();
103+
104+
$patches = array();
105+
foreach ($results as $r) {
106+
$matches = array();
107+
if (preg_match('/(\d+)\..*\.sql$/', $r, $matches)) {
108+
$patches[] = array('version' => (int)$matches[1],
109+
'file' => $r);
110+
} else {
111+
print
112+
"*** WARNING : File {$r} does not follow the normal naming ".
113+
"convention. ***\n";
114+
}
113115
}
114-
}
115-
116-
// Files are in some 'random' order returned by the operating system
117-
// We need to apply them in proper order
118-
$patches = isort($patches, 'version');
119116

120-
$patch_applied = false;
121-
foreach ($patches as $patch) {
122-
if ($patch['version'] < $next_version) {
123-
continue;
117+
// Files are in some 'random' order returned by the operating system
118+
// We need to apply them in proper order
119+
$patches = isort($patches, 'version');
120+
121+
$patch_applied = false;
122+
foreach ($patches as $patch) {
123+
if ($patch['version'] < $next_version) {
124+
continue;
125+
}
126+
127+
print "Applying patch {$patch['file']}\n";
128+
129+
$path = Filesystem::resolvePath($patches_dir.$patch['file']);
130+
131+
list($stdout, $stderr) = execx(
132+
"mysql --user=%s --password=%s --host=%s < %s",
133+
$conn_user,
134+
$conn_pass,
135+
$conn_host,
136+
$path);
137+
138+
if ($stderr) {
139+
print $stderr;
140+
exit(-1);
141+
}
142+
143+
// Patch was successful, update the db with the latest applied patch version
144+
// 'DELETE' and 'INSERT' instead of update, because the table might be empty
145+
queryfx(
146+
$conn,
147+
'DELETE FROM phabricator_meta_data.%T',
148+
SCHEMA_VERSION_TABLE_NAME);
149+
queryfx(
150+
$conn,
151+
'INSERT INTO phabricator_meta_data.%T VALUES (%d)',
152+
SCHEMA_VERSION_TABLE_NAME,
153+
$patch['version']);
154+
155+
$patch_applied = true;
124156
}
125157

126-
print "Applying patch {$patch['file']}\n";
127-
128-
$path = Filesystem::resolvePath($patches_dir.$patch['file']);
129-
130-
$user = PhabricatorEnv::getEnvConfig('mysql.user');
131-
$pass = PhabricatorEnv::getEnvConfig('mysql.pass');
132-
$host = PhabricatorEnv::getEnvConfig('mysql.host');
133-
134-
list($stdout, $stderr) = execx(
135-
"mysql --user=%s --password=%s --host=%s < %s",
136-
$user, $pass, $host, $path);
137-
138-
if ($stderr) {
139-
print $stderr;
140-
exit(-1);
158+
if (!$patch_applied) {
159+
print "Your database is already up-to-date.\n";
141160
}
142161

143-
// Patch was successful, update the db with the latest applied patch version
144-
// 'DELETE' and 'INSERT' instead of update, because the table might be empty
145-
queryfx($conn, 'DELETE FROM %T', SCHEMA_VERSION_TABLE_NAME);
146-
queryfx(
147-
$conn,
148-
'INSERT INTO %T values (%d)',
149-
SCHEMA_VERSION_TABLE_NAME,
150-
$patch['version']);
151-
152-
$patch_applied = true;
162+
} catch (AphrontQueryAccessDeniedException $ex) {
163+
echo
164+
"ACCESS DENIED\n".
165+
"The user '{$conn_user}' does not have sufficient MySQL privileges to\n".
166+
"execute the schema upgrade. Use the -u and -p flags to run as a user\n".
167+
"with more privileges (e.g., root).".
168+
"\n\n".
169+
"EXCEPTION:\n".
170+
$ex->getMessage().
171+
"\n\n";
172+
exit(1);
153173
}
154174

155-
if (!$patch_applied) {
156-
print "Your database is already up-to-date.\n";
175+
function usage() {
176+
echo
177+
"usage: upgrade_schema.php [-v version] [-u user -p pass]".
178+
"\n\n".
179+
"Run 'upgrade_schema.php -v 12' to apply all patches starting from ".
180+
"version 12.\n".
181+
"Run 'upgrade_schema.php -u root -p hunter2' to override the configured ".
182+
"default user.\n";
183+
exit(1);
157184
}
185+

‎src/__phutil_library_map__.php

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
'AphrontPageView' => 'view/page/base',
4646
'AphrontPagerView' => 'view/control/pager',
4747
'AphrontPanelView' => 'view/layout/panel',
48+
'AphrontQueryAccessDeniedException' => 'storage/exception/accessdenied',
4849
'AphrontQueryConnectionException' => 'storage/exception/connection',
4950
'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost',
5051
'AphrontQueryCountException' => 'storage/exception/count',
@@ -509,6 +510,7 @@
509510
'AphrontPageView' => 'AphrontView',
510511
'AphrontPagerView' => 'AphrontView',
511512
'AphrontPanelView' => 'AphrontView',
513+
'AphrontQueryAccessDeniedException' => 'AphrontQueryRecoverableException',
512514
'AphrontQueryConnectionException' => 'AphrontQueryException',
513515
'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException',
514516
'AphrontQueryCountException' => 'AphrontQueryException',

‎src/docs/configuration_guide.diviner

+2-1
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,5 @@ change by providing overrides in ##myconfig.conf.php##.
119119
= Upgrading Schema =
120120

121121
After you have configured Phabricator, you need to upgrade the database
122-
schema, see @{article:Upgrading Schema}
122+
schema, see @{article:Upgrading Schema}. You'll also need to do this after you
123+
update the code in the future.

‎src/docs/upgrade_schema.diviner

+9-3
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,25 @@ configured your Phabricator environment. If you haven't, see
1414
If you are doing this for the first time to a freshly installed MySQL database,
1515
run the following command:
1616

17-
PHABRICATOR_ENV=<your_config> php path/to/phabricator/scripts/sql/upgrade_schema.php 0
17+
PHABRICATOR_ENV=<your_config> path/to/phabricator/scripts/sql/upgrade_schema.php -v 0
1818

1919
This will install all the patches starting from 0. Running this script will
2020
store the information of the latest installed patch in the Phabricator database.
2121
Next time you want to upgrade your schema, just run:
2222

23-
PHABRICATOR_ENV=<your_config> php path/to/phabricator/scripts/sql/upgrade_schema.php
23+
PHABRICATOR_ENV=<your_config> path/to/phabricator/scripts/sql/upgrade_schema.php
2424

2525
This will install all the patches that are new since the last time you ran
2626
this script.
2727

28+
If your configuration uses an unprivileged user to connect to the database, you
29+
may have to override the default user so the schema changes can be applied with
30+
root or some other admin user:
31+
32+
PHABRICATOR_ENV=<your_config> path/to/phabricator/scripts/sql/upgrade_schema.php -u <user> -p <pass>
33+
2834
If you need to upgrade the schema starting from a specific patch, just run:
2935

30-
PHABRICATOR_ENV=<your_config> php path/to/phabricator/scripts/sql/upgrade_schema.php <patch_number>
36+
PHABRICATOR_ENV=<your_config> path/to/phabricator/scripts/sql/upgrade_schema.php -v <patch_number>
3137

3238
However, this isn't usually needed.

‎src/storage/connection/mysql/AphrontMySQLDatabaseConnection.php

+10-3
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,11 @@ private function establishConnection() {
144144
"{$error}.");
145145
}
146146

147-
$ret = @mysql_select_db($database, $conn);
148-
if (!$ret) {
149-
$this->throwQueryException($conn);
147+
if ($database !== null) {
148+
$ret = @mysql_select_db($database, $conn);
149+
if (!$ret) {
150+
$this->throwQueryException($conn);
151+
}
150152
}
151153

152154
$end = microtime(true);
@@ -249,6 +251,11 @@ private function throwQueryException($connection) {
249251
// portable to parse the key out of the error and attach it to the
250252
// exception.
251253
throw new AphrontQueryDuplicateKeyException("{$errno}: {$error}");
254+
case 1044: // Access denied to database
255+
case 1045: // Access denied (auth)
256+
case 1142: // Access denied to table
257+
case 1143: // Access denied to column
258+
throw new AphrontQueryAccessDeniedException("#{$errno}: {$error}");
252259
default:
253260
// TODO: 1064 is syntax error, and quite terrible in production.
254261
throw new AphrontQueryException("#{$errno}: {$error}");

‎src/storage/connection/mysql/__init__.php

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
phutil_require_module('phabricator', 'aphront/console/plugin/services/api');
1010
phutil_require_module('phabricator', 'storage/connection/base');
11+
phutil_require_module('phabricator', 'storage/exception/accessdenied');
1112
phutil_require_module('phabricator', 'storage/exception/base');
1213
phutil_require_module('phabricator', 'storage/exception/connection');
1314
phutil_require_module('phabricator', 'storage/exception/connectionlost');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* Copyright 2011 Facebook, Inc.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* @group storage
21+
*/
22+
class AphrontQueryAccessDeniedException
23+
extends AphrontQueryRecoverableException { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
/**
3+
* This file is automatically generated. Lint this module to rebuild it.
4+
* @generated
5+
*/
6+
7+
8+
9+
phutil_require_module('phabricator', 'storage/exception/recoverable');
10+
11+
12+
phutil_require_source('AphrontQueryAccessDeniedException.php');

0 commit comments

Comments
 (0)
Failed to load comments.