1<?php
2/**
3 * Get user's global privileges and some db-specific privileges
4 */
5
6declare(strict_types=1);
7
8namespace PhpMyAdmin;
9
10use PhpMyAdmin\Query\Utilities;
11use PhpMyAdmin\Utils\SessionCache;
12use function mb_strpos;
13use function mb_substr;
14use function preg_match;
15use function preg_replace;
16use function strpos;
17
18/**
19 * PhpMyAdmin\CheckUserPrivileges class
20 */
21class CheckUserPrivileges
22{
23    /** @var DatabaseInterface */
24    private $dbi;
25
26    /**
27     * @param DatabaseInterface $dbi DatabaseInterface object
28     */
29    public function __construct(DatabaseInterface $dbi)
30    {
31        $this->dbi = $dbi;
32    }
33
34    /**
35     * Extracts details from a result row of a SHOW GRANT query
36     *
37     * @param string $row grant row
38     *
39     * @return array
40     */
41    public function getItemsFromShowGrantsRow(string $row): array
42    {
43        $db_name_offset = mb_strpos($row, ' ON ') + 4;
44
45        $tblname_end_offset = mb_strpos($row, ' TO ');
46        $tblname_start_offset = false;
47        $__tblname_start_offset = mb_strpos($row, '`.', $db_name_offset);
48
49        if ($__tblname_start_offset && $__tblname_start_offset < $tblname_end_offset) {
50            $tblname_start_offset = $__tblname_start_offset + 1;
51        }
52
53        if ($tblname_start_offset === false) {
54            $tblname_start_offset = mb_strpos($row, '.', $db_name_offset);
55        }
56
57        $show_grants_dbname = mb_substr(
58            $row,
59            $db_name_offset,
60            $tblname_start_offset - $db_name_offset
61        );
62
63        $show_grants_dbname = Util::unQuote($show_grants_dbname, '`');
64
65        $show_grants_str = mb_substr(
66            $row,
67            6,
68            mb_strpos($row, ' ON ') - 6
69        );
70
71        $show_grants_tblname = mb_substr(
72            $row,
73            $tblname_start_offset + 1,
74            $tblname_end_offset - $tblname_start_offset - 1
75        );
76        $show_grants_tblname = Util::unQuote($show_grants_tblname, '`');
77
78        return [
79            $show_grants_str,
80            $show_grants_dbname,
81            $show_grants_tblname,
82        ];
83    }
84
85    /**
86     * Check if user has required privileges for
87     * performing 'Adjust privileges' operations
88     *
89     * @param string $show_grants_str     string containing grants for user
90     * @param string $show_grants_dbname  name of db extracted from grant string
91     * @param string $show_grants_tblname name of table extracted from grant string
92     */
93    public function checkRequiredPrivilegesForAdjust(
94        string $show_grants_str,
95        string $show_grants_dbname,
96        string $show_grants_tblname
97    ): void {
98        // '... ALL PRIVILEGES ON *.* ...' OR '... ALL PRIVILEGES ON `mysql`.* ..'
99        // OR
100        // SELECT, INSERT, UPDATE, DELETE .... ON *.* OR `mysql`.*
101        if ($show_grants_str !== 'ALL'
102            && $show_grants_str !== 'ALL PRIVILEGES'
103            && (mb_strpos(
104                $show_grants_str,
105                'SELECT, INSERT, UPDATE, DELETE'
106            ) === false)
107        ) {
108            return;
109        }
110
111        if ($show_grants_dbname === '*'
112            && $show_grants_tblname === '*'
113        ) {
114            $GLOBALS['col_priv'] = true;
115            $GLOBALS['db_priv'] = true;
116            $GLOBALS['proc_priv'] = true;
117            $GLOBALS['table_priv'] = true;
118
119            if ($show_grants_str === 'ALL PRIVILEGES'
120                || $show_grants_str === 'ALL'
121            ) {
122                $GLOBALS['is_reload_priv'] = true;
123            }
124        }
125
126        // check for specific tables in `mysql` db
127        // Ex. '... ALL PRIVILEGES on `mysql`.`columns_priv` .. '
128        if ($show_grants_dbname !== 'mysql') {
129            return;
130        }
131
132        switch ($show_grants_tblname) {
133            case 'columns_priv':
134                $GLOBALS['col_priv'] = true;
135                break;
136            case 'db':
137                $GLOBALS['db_priv'] = true;
138                break;
139            case 'procs_priv':
140                $GLOBALS['proc_priv'] = true;
141                break;
142            case 'tables_priv':
143                $GLOBALS['table_priv'] = true;
144                break;
145            case '*':
146                $GLOBALS['col_priv'] = true;
147                $GLOBALS['db_priv'] = true;
148                $GLOBALS['proc_priv'] = true;
149                $GLOBALS['table_priv'] = true;
150                break;
151            default:
152        }
153    }
154
155    /**
156     * sets privilege information extracted from SHOW GRANTS result
157     *
158     * Detection for some CREATE privilege.
159     *
160     * Since MySQL 4.1.2, we can easily detect current user's grants using $userlink
161     * (no control user needed) and we don't have to try any other method for
162     * detection
163     *
164     * @todo fix to get really all privileges, not only explicitly defined for this user
165     * from MySQL manual: (https://dev.mysql.com/doc/refman/5.0/en/show-grants.html)
166     * SHOW GRANTS displays only the privileges granted explicitly to the named
167     * account. Other privileges might be available to the account, but they are not
168     * displayed. For example, if an anonymous account exists, the named account
169     * might be able to use its privileges, but SHOW GRANTS will not display them.
170     */
171    private function analyseShowGrant(): void
172    {
173        if (SessionCache::has('is_create_db_priv')) {
174            $GLOBALS['is_create_db_priv'] = SessionCache::get(
175                'is_create_db_priv'
176            );
177            $GLOBALS['is_reload_priv'] = SessionCache::get(
178                'is_reload_priv'
179            );
180            $GLOBALS['db_to_create'] = SessionCache::get(
181                'db_to_create'
182            );
183            $GLOBALS['dbs_where_create_table_allowed'] = SessionCache::get(
184                'dbs_where_create_table_allowed'
185            );
186            $GLOBALS['dbs_to_test'] = SessionCache::get(
187                'dbs_to_test'
188            );
189
190            $GLOBALS['db_priv'] = SessionCache::get(
191                'db_priv'
192            );
193            $GLOBALS['col_priv'] = SessionCache::get(
194                'col_priv'
195            );
196            $GLOBALS['table_priv'] = SessionCache::get(
197                'table_priv'
198            );
199            $GLOBALS['proc_priv'] = SessionCache::get(
200                'proc_priv'
201            );
202
203            return;
204        }
205
206        // defaults
207        $GLOBALS['is_create_db_priv']  = false;
208        $GLOBALS['is_reload_priv'] = false;
209        $GLOBALS['db_to_create'] = '';
210        $GLOBALS['dbs_where_create_table_allowed'] = [];
211        $GLOBALS['dbs_to_test'] = Utilities::getSystemSchemas();
212        $GLOBALS['proc_priv'] = false;
213        $GLOBALS['db_priv'] = false;
214        $GLOBALS['col_priv'] = false;
215        $GLOBALS['table_priv'] = false;
216
217        $rs_usr = $this->dbi->tryQuery('SHOW GRANTS');
218
219        if (! $rs_usr) {
220            return;
221        }
222
223        $re0 = '(^|(\\\\\\\\)+|[^\\\\])'; // non-escaped wildcards
224        $re1 = '(^|[^\\\\])(\\\)+'; // escaped wildcards
225
226        while ($row = $this->dbi->fetchRow($rs_usr)) {
227            [
228                $show_grants_str,
229                $show_grants_dbname,
230                $show_grants_tblname,
231            ] = $this->getItemsFromShowGrantsRow($row[0]);
232
233            if ($show_grants_dbname === '*') {
234                if ($show_grants_str !== 'USAGE') {
235                    $GLOBALS['dbs_to_test'] = false;
236                }
237            } elseif ($GLOBALS['dbs_to_test'] !== false) {
238                $GLOBALS['dbs_to_test'][] = $show_grants_dbname;
239            }
240
241            if (mb_strpos($show_grants_str, 'RELOAD') !== false) {
242                $GLOBALS['is_reload_priv'] = true;
243            }
244
245            // check for the required privileges for adjust
246            $this->checkRequiredPrivilegesForAdjust(
247                $show_grants_str,
248                $show_grants_dbname,
249                $show_grants_tblname
250            );
251
252            /**
253             * @todo if we find CREATE VIEW but not CREATE, do not offer
254             * the create database dialog box
255             */
256            if ($show_grants_str !== 'ALL'
257                && $show_grants_str !== 'ALL PRIVILEGES'
258                && $show_grants_str !== 'CREATE'
259                && strpos($show_grants_str, 'CREATE,') === false
260            ) {
261                continue;
262            }
263
264            if ($show_grants_dbname === '*') {
265                // a global CREATE privilege
266                $GLOBALS['is_create_db_priv'] = true;
267                $GLOBALS['is_reload_priv'] = true;
268                $GLOBALS['db_to_create']   = '';
269                $GLOBALS['dbs_where_create_table_allowed'][] = '*';
270                // @todo we should not break here, cause GRANT ALL *.*
271                // could be revoked by a later rule like GRANT SELECT ON db.*
272                break;
273            }
274
275            // this array may contain wildcards
276            $GLOBALS['dbs_where_create_table_allowed'][] = $show_grants_dbname;
277
278            $dbname_to_test = Util::backquote($show_grants_dbname);
279
280            if ($GLOBALS['is_create_db_priv']) {
281                // no need for any more tests if we already know this
282                continue;
283            }
284
285            // does this db exist?
286            if ((! preg_match('/' . $re0 . '%|_/', $show_grants_dbname)
287                || preg_match('/\\\\%|\\\\_/', $show_grants_dbname))
288                && ($this->dbi->tryQuery(
289                    'USE ' . preg_replace(
290                        '/' . $re1 . '(%|_)/',
291                        '\\1\\3',
292                        $dbname_to_test
293                    )
294                )
295                || mb_substr((string) $this->dbi->getError(), 1, 4) == 1044)
296            ) {
297                continue;
298            }
299
300            /**
301             * Do not handle the underscore wildcard
302             * (this case must be rare anyway)
303             */
304            $GLOBALS['db_to_create'] = preg_replace(
305                '/' . $re0 . '%/',
306                '\\1',
307                $show_grants_dbname
308            );
309            $GLOBALS['db_to_create'] = preg_replace(
310                '/' . $re1 . '(%|_)/',
311                '\\1\\3',
312                $GLOBALS['db_to_create']
313            );
314            $GLOBALS['is_create_db_priv'] = true;
315
316            /**
317             * @todo collect $GLOBALS['db_to_create'] into an array,
318             * to display a drop-down in the "Create database" dialog
319             */
320             // we don't break, we want all possible databases
321             //break;
322        }
323
324        $this->dbi->freeResult($rs_usr);
325
326        // must also cacheUnset() them in
327        // PhpMyAdmin\Plugins\Auth\AuthenticationCookie
328        SessionCache::set('is_create_db_priv', $GLOBALS['is_create_db_priv']);
329        SessionCache::set('is_reload_priv', $GLOBALS['is_reload_priv']);
330        SessionCache::set('db_to_create', $GLOBALS['db_to_create']);
331        SessionCache::set(
332            'dbs_where_create_table_allowed',
333            $GLOBALS['dbs_where_create_table_allowed']
334        );
335        SessionCache::set('dbs_to_test', $GLOBALS['dbs_to_test']);
336
337        SessionCache::set('proc_priv', $GLOBALS['proc_priv']);
338        SessionCache::set('table_priv', $GLOBALS['table_priv']);
339        SessionCache::set('col_priv', $GLOBALS['col_priv']);
340        SessionCache::set('db_priv', $GLOBALS['db_priv']);
341    }
342
343    /**
344     * Get user's global privileges and some db-specific privileges
345     */
346    public function getPrivileges(): void
347    {
348        $username = '';
349
350        $current = $this->dbi->getCurrentUserAndHost();
351        if (! empty($current)) {
352            [$username] = $current;
353        }
354
355        // If MySQL is started with --skip-grant-tables
356        if ($username === '') {
357            $GLOBALS['is_create_db_priv'] = true;
358            $GLOBALS['is_reload_priv'] = true;
359            $GLOBALS['db_to_create'] = '';
360            $GLOBALS['dbs_where_create_table_allowed'] = ['*'];
361            $GLOBALS['dbs_to_test'] = false;
362            $GLOBALS['db_priv'] = true;
363            $GLOBALS['col_priv'] = true;
364            $GLOBALS['table_priv'] = true;
365            $GLOBALS['proc_priv'] = true;
366        } else {
367            $this->analyseShowGrant();
368        }
369    }
370}
371