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