UserPrivilegesFactory   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 227
Duplicated Lines 0 %

Test Coverage

Coverage 72.88%

Importance

Changes 0
Metric Value
wmc 36
eloc 112
dl 0
loc 227
ccs 86
cts 118
cp 0.7288
rs 9.52
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A getPrivileges() 0 21 3
D analyseShowGrant() 0 122 18
C checkRequiredPrivilegesForAdjust() 0 50 14
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin;
6
7
use PhpMyAdmin\Dbal\DatabaseInterface;
8
use PhpMyAdmin\Query\Utilities;
9
use PhpMyAdmin\Utils\SessionCache;
10
11
use function mb_substr;
12
use function preg_match;
13
use function preg_replace;
14
use function str_contains;
15
16
/**
17
 * Get user's global privileges and some db-specific privileges
18
 */
19
class UserPrivilegesFactory
20
{
21 24
    public function __construct(private DatabaseInterface $dbi)
22
    {
23 24
    }
24
25
    /**
26
     * Check if user has required privileges for
27
     * performing 'Adjust privileges' operations
28
     */
29 8
    public function checkRequiredPrivilegesForAdjust(UserPrivileges $userPrivileges, ShowGrants $showGrants): void
30
    {
31
        // '... ALL PRIVILEGES ON *.* ...' OR '... ALL PRIVILEGES ON `mysql`.* ..'
32
        // OR
33
        // SELECT, INSERT, UPDATE, DELETE .... ON *.* OR `mysql`.*
34
        if (
35 8
            $showGrants->grants !== 'ALL'
36 8
            && $showGrants->grants !== 'ALL PRIVILEGES'
37 8
            && ! str_contains($showGrants->grants, 'SELECT, INSERT, UPDATE, DELETE')
38
        ) {
39
            return;
40
        }
41
42 8
        if ($showGrants->dbName === '*' && $showGrants->tableName === '*') {
43 8
            $userPrivileges->column = true;
44 8
            $userPrivileges->database = true;
45 8
            $userPrivileges->routines = true;
46 8
            $userPrivileges->table = true;
47
48 8
            if ($showGrants->grants === 'ALL PRIVILEGES' || $showGrants->grants === 'ALL') {
49 8
                $userPrivileges->isReload = true;
50
            }
51
        }
52
53
        // check for specific tables in `mysql` db
54
        // Ex. '... ALL PRIVILEGES on `mysql`.`columns_priv` .. '
55 8
        if ($showGrants->dbName !== 'mysql') {
56 8
            return;
57
        }
58
59 4
        switch ($showGrants->tableName) {
60 4
            case 'columns_priv':
61
                $userPrivileges->column = true;
62
                break;
63 4
            case 'db':
64 4
                $userPrivileges->database = true;
65 4
                break;
66 4
            case 'procs_priv':
67
                $userPrivileges->routines = true;
68
                break;
69 4
            case 'tables_priv':
70
                $userPrivileges->table = true;
71
                break;
72 4
            case '*':
73 4
                $userPrivileges->column = true;
74 4
                $userPrivileges->database = true;
75 4
                $userPrivileges->routines = true;
76 4
                $userPrivileges->table = true;
77 4
                break;
78
            default:
79
        }
80
    }
81
82
    /**
83
     * sets privilege information extracted from SHOW GRANTS result
84
     *
85
     * Detection for some CREATE privilege.
86
     *
87
     * Since MySQL 4.1.2, we can easily detect current user's grants using $userlink
88
     * (no control user needed) and we don't have to try any other method for
89
     * detection
90
     *
91
     * @todo fix to get really all privileges, not only explicitly defined for this user
92
     * from MySQL manual: (https://dev.mysql.com/doc/refman/5.0/en/show-grants.html)
93
     * SHOW GRANTS displays only the privileges granted explicitly to the named
94
     * account. Other privileges might be available to the account, but they are not
95
     * displayed. For example, if an anonymous account exists, the named account
96
     * might be able to use its privileges, but SHOW GRANTS will not display them.
97
     */
98 16
    private function analyseShowGrant(): UserPrivileges
99
    {
100 16
        if (SessionCache::has('is_create_db_priv')) {
101 4
            return new UserPrivileges(
102 4
                SessionCache::get('db_priv'),
103 4
                SessionCache::get('table_priv'),
104 4
                SessionCache::get('col_priv'),
105 4
                SessionCache::get('proc_priv'),
106 4
                SessionCache::get('is_reload_priv'),
107 4
                SessionCache::get('is_create_db_priv'),
108 4
                SessionCache::get('db_to_create'),
109 4
                SessionCache::get('dbs_to_test'),
110 4
            );
111
        }
112
113 12
        $showGrantsResult = $this->dbi->tryQuery('SHOW GRANTS');
114 12
        if (! $showGrantsResult) {
115 4
            return new UserPrivileges(databasesToTest: Utilities::getSystemSchemas());
116
        }
117
118 8
        $userPrivileges = new UserPrivileges(databasesToTest: Utilities::getSystemSchemas());
119
120 8
        $re0 = '(^|(\\\\\\\\)+|[^\\\\])'; // non-escaped wildcards
121 8
        $re1 = '(^|[^\\\\])(\\\)+'; // escaped wildcards
122
123 8
        while ($showGrants = $showGrantsResult->fetchValue()) {
124 4
            $showGrants = new ShowGrants($showGrants);
125
126 4
            if ($showGrants->dbName === '*') {
127 4
                if ($showGrants->grants !== 'USAGE') {
128 4
                    $userPrivileges->databasesToTest = false;
129
                }
130
            } elseif ($userPrivileges->databasesToTest !== false) {
131
                $userPrivileges->databasesToTest[] = $showGrants->dbName;
132
            }
133
134 4
            if (str_contains($showGrants->grants, 'RELOAD')) {
135
                $userPrivileges->isReload = true;
136
            }
137
138
            // check for the required privileges for adjust
139 4
            $this->checkRequiredPrivilegesForAdjust($userPrivileges, $showGrants);
140
141
            /**
142
             * @todo if we find CREATE VIEW but not CREATE, do not offer
143
             * the create database dialog box
144
             */
145
            if (
146 4
                $showGrants->grants !== 'ALL'
147 4
                && $showGrants->grants !== 'ALL PRIVILEGES'
148 4
                && $showGrants->grants !== 'CREATE'
149 4
                && ! str_contains($showGrants->grants, 'CREATE,')
150
            ) {
151
                continue;
152
            }
153
154 4
            if ($showGrants->dbName === '*') {
155
                // a global CREATE privilege
156 4
                $userPrivileges->isCreateDatabase = true;
157 4
                $userPrivileges->isReload = true;
158 4
                $userPrivileges->databaseToCreate = '';
159
                // @todo we should not break here, cause GRANT ALL *.*
160
                // could be revoked by a later rule like GRANT SELECT ON db.*
161 4
                break;
162
            }
163
164
            $dbNameToTest = Util::backquote($showGrants->dbName);
165
166
            if ($userPrivileges->isCreateDatabase) {
167
                // no need for any more tests if we already know this
168
                continue;
169
            }
170
171
            // does this db exist?
172
            if (
173
                (preg_match('/' . $re0 . '%|_/', $showGrants->dbName) !== 1
174
                || preg_match('/\\\\%|\\\\_/', $showGrants->dbName) === 1)
175
                && ($this->dbi->tryQuery(
176
                    'USE ' . preg_replace(
177
                        '/' . $re1 . '(%|_)/',
178
                        '\\1\\3',
179
                        $dbNameToTest,
180
                    ),
181
                )
182
                || mb_substr($this->dbi->getError(), 1, 4) == 1044)
183
            ) {
184
                continue;
185
            }
186
187
            /**
188
             * Do not handle the underscore wildcard
189
             * (this case must be rare anyway)
190
             */
191
            $userPrivileges->databaseToCreate = preg_replace('/' . $re0 . '%/', '\\1', $showGrants->dbName);
192
            $userPrivileges->databaseToCreate = preg_replace(
193
                '/' . $re1 . '(%|_)/',
194
                '\\1\\3',
195
                $userPrivileges->databaseToCreate,
196
            );
197
            $userPrivileges->isCreateDatabase = true;
198
199
            /**
200
             * @todo collect {@see UserPrivileges::$databaseToCreate} into an array,
201
             * to display a drop-down in the "Create database" dialog
202
             */
203
            // we don't break, we want all possible databases
204
            //break;
205
        }
206
207
        // must also cacheUnset() them in
208
        // PhpMyAdmin\Plugins\Auth\AuthenticationCookie
209 8
        SessionCache::set('is_create_db_priv', $userPrivileges->isCreateDatabase);
210 8
        SessionCache::set('is_reload_priv', $userPrivileges->isReload);
211 8
        SessionCache::set('db_to_create', $userPrivileges->databaseToCreate);
212 8
        SessionCache::set('dbs_to_test', $userPrivileges->databasesToTest);
213
214 8
        SessionCache::set('proc_priv', $userPrivileges->routines);
215 8
        SessionCache::set('table_priv', $userPrivileges->table);
216 8
        SessionCache::set('col_priv', $userPrivileges->column);
217 8
        SessionCache::set('db_priv', $userPrivileges->database);
218
219 8
        return $userPrivileges;
220
    }
221
222
    /**
223
     * Get user's global privileges and some db-specific privileges
224
     */
225 20
    public function getPrivileges(): UserPrivileges
226
    {
227 20
        $username = '';
228 20
        $current = $this->dbi->getCurrentUserAndHost();
229 20
        if ($current !== []) {
230 20
            [$username] = $current;
231
        }
232
233
        // If MySQL is started with --skip-grant-tables
234 20
        if ($username === '') {
235 4
            return new UserPrivileges(
236 4
                database: true,
237 4
                table: true,
238 4
                column: true,
239 4
                routines: true,
240 4
                isReload: true,
241 4
                isCreateDatabase: true,
242 4
            );
243
        }
244
245 16
        return $this->analyseShowGrant();
246
    }
247
}
248