AuthModel::buildUserFoldersList()   C
last analyzed

Complexity

Conditions 17
Paths 16

Size

Total Lines 109
Code Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 69
c 0
b 0
f 0
nc 16
nop 1
dl 0
loc 109
rs 5.2166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This file is part of the TeamPass project.
6
 * 
7
 * TeamPass is free software: you can redistribute it and/or modify it
8
 * under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, version 3 of the License.
10
 * 
11
 * TeamPass is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 * 
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 * 
19
 * Certain components of this file may be under different licenses. For
20
 * details, see the `licenses` directory or individual file headers.
21
 * ---
22
 * @version    API
23
 *
24
 * @file      AuthModel.php
25
 * @author    Nils Laumaillé ([email protected])
26
 * @copyright 2009-2025 Teampass.net
27
 * @license   GPL-3.0
28
 * @see       https://www.teampass.net
29
 */
30
31
use TeampassClasses\PasswordManager\PasswordManager;
32
use TeampassClasses\NestedTree\NestedTree;
33
use TeampassClasses\ConfigManager\ConfigManager;
34
use Firebase\JWT\JWT;
35
use Firebase\JWT\Key;
36
37
class AuthModel
38
{
39
40
41
    /**
42
     * Is the user allowed
43
     *
44
     * @param string $login
45
     * @param string $password
46
     * @param string $apikey
47
     * @return array
48
     */
49
    public function getUserAuth(string $login, string $password, string $apikey): array
50
    {
51
        // Sanitize
52
        include_once API_ROOT_PATH . '/../sources/main.functions.php';
53
        $inputData = dataSanitizer(
54
            [
55
                'login' => isset($login) === true ? $login : '',
56
                'password' => isset($password) === true ? $password : '',
57
                'apikey' => isset($apikey) === true ? $apikey : '',
58
            ],
59
            [
60
                'login' => 'trim|escape|strip_tags',
61
                'password' => 'trim|escape',
62
                'apikey' => 'trim|escape|strip_tags',
63
            ]
64
        );
65
66
        // Check apikey and credentials
67
        if (empty($inputData['login']) === true || empty($inputData['apikey']) === true || empty($inputData['password']) === true) {
68
            // case where it is a generic key
69
            // Not allowed to use this API
70
71
            return ["error" => "Login failed.", "info" => "User password is requested"];
72
        } else {
73
            // case where it is a user api key
74
            // Check if user exists
75
            $userInfo = DB::queryfirstrow(
76
                "SELECT u.id, u.pw, u.login, u.admin, u.gestionnaire, u.can_manage_all_users, u.fonction_id, u.can_create_root_folder, u.public_key, u.private_key, u.personal_folder, u.fonction_id, u.groupes_visibles, u.groupes_interdits, a.value AS user_api_key, a.allowed_folders as user_api_allowed_folders, a.enabled, a.allowed_to_create, a.allowed_to_read, a.allowed_to_update, a.allowed_to_delete
77
                FROM " . prefixTable('users') . " AS u
78
                INNER JOIN " . prefixTable('api') . " AS a ON (a.user_id=u.id)
79
                WHERE login = %s",
80
                $inputData['login']
81
            );
82
            if (DB::count() === 0) {
83
                return ["error" => "Login failed.", "info" => "apikey : Not valid"];
84
            }
85
86
            // Check if user is enabled
87
            if ((int) $userInfo['enabled'] === 0) {
88
                return ["error" => "Login failed.", "info" => "User not allowed to use API"];
89
            }
90
            
91
            // Check password
92
            $passwordManager = new PasswordManager();
93
            if ($passwordManager->verifyPassword($userInfo['pw'], $inputData['password']) === true) {
94
                // Correct credentials
95
                // get user keys
96
                $privateKeyClear = decryptPrivateKey($inputData['password'], (string) $userInfo['private_key']);
97
98
                // check API key
99
                if ($inputData['apikey'] !== base64_decode(decryptUserObjectKey($userInfo['user_api_key'], $privateKeyClear))) {
100
                    return ["error" => "Login failed.", "apikey" => "Not valid"];
101
                }
102
103
                // Update user's key_tempo
104
                $keyTempo = bin2hex(random_bytes(16));
105
                DB::update(
106
                    prefixTable('users'),
107
                    [
108
                        'key_tempo' => $keyTempo,
109
                    ],
110
                    'id = %i',
111
                    $userInfo['id']
112
                );
113
114
                // Generate a unique session key for this API session (256 bits / 32 bytes)
115
                // This key will be stored in the JWT and used to decrypt the private key
116
                $sessionKey = random_bytes(32);
117
                $sessionKeySalt = bin2hex(random_bytes(16));
118
119
                // Encrypt the decrypted private key with the session key
120
                // This allows us to store it securely in the database without exposing it
121
                require_once API_ROOT_PATH . '/inc/encryption_utils.php';
122
                $encryptedPrivateKey = encrypt_with_session_key($privateKeyClear, $sessionKey);
123
124
                if ($encryptedPrivateKey === false) {
125
                    return ["error" => "Login failed.", "info" => "Failed to encrypt private key"];
126
                }
127
128
                // Store the ENCRYPTED private key in the API table
129
                // Even if the database is compromised, the key cannot be used without the session_key from the JWT
130
                DB::update(
131
                    prefixTable('api'),
132
                    [
133
                        'encrypted_private_key' => $encryptedPrivateKey,
134
                        'session_key_salt' => $sessionKeySalt,
135
                        'timestamp' => time(),
136
                    ],
137
                    'user_id = %i',
138
                    $userInfo['id']
139
                );
140
141
                // get user folders list
142
                $ret = $this->buildUserFoldersList($userInfo);
143
144
                // Load config
145
                $configManager = new ConfigManager();
146
                $SETTINGS = $configManager->getAllSettings();
147
148
                // Log user
149
                logEvents($SETTINGS, 'api', 'user_connection', (string) $userInfo['id'], stripslashes($userInfo['login']));
150
151
                // create JWT with session key
152
                return $this->createUserJWT(
153
                    (int) $userInfo['id'],
154
                    (string) $inputData['login'],
155
                    (int) $userInfo['personal_folder'],
156
                    (string) implode(",", $ret['folders']),
157
                    (string) implode(",", $ret['items']),
158
                    (string) $keyTempo,
159
                    (string) base64_encode($sessionKey), // Session key for decrypting private key
160
                    (int) $userInfo['admin'],
161
                    (int) $userInfo['gestionnaire'],
162
                    (int) $userInfo['can_create_root_folder'],
163
                    (int) $userInfo['can_manage_all_users'],
164
                    (string) $userInfo['fonction_id'],
165
                    (string) $userInfo['user_api_allowed_folders'],
166
                    (int) $userInfo['allowed_to_create'],
167
                    (int) $userInfo['allowed_to_read'],
168
                    (int) $userInfo['allowed_to_update'],
169
                    (int) $userInfo['allowed_to_delete'],
170
                );
171
            } else {
172
                return ["error" => "Login failed.", "info" => "password : Not valid"];
173
            }
174
        }
175
    }
176
    //end getUserAuth
177
178
    /**
179
     * Create a JWT
180
     *
181
     * Note: User encryption keys (public_key, private_key) are no longer stored in the JWT
182
     * to reduce token size. Instead, the private key is encrypted with a session key and
183
     * stored in the database. The session key is stored in the JWT (~44 bytes) which is
184
     * used to decrypt the private key when needed.
185
     *
186
     * Security approach (defense in depth):
187
     * - Encrypted private key in DB is useless without session_key
188
     * - session_key in JWT is useless without encrypted private key from DB
189
     * - Both are required together to access the decrypted private key
190
     *
191
     * @param integer $id
192
     * @param string $login
193
     * @param integer $pf_enabled
194
     * @param string $folders
195
     * @param string $items
196
     * @param string $keyTempo
197
     * @param string $sessionKey Session key (base64) for decrypting private key from DB
198
     * @param integer $admin
199
     * @param integer $manager
200
     * @param integer $can_create_root_folder
201
     * @param integer $can_manage_all_users
202
     * @param string $roles
203
     * @param string $allowed_folders
204
     * @param integer $allowed_to_create
205
     * @param integer $allowed_to_read
206
     * @param integer $allowed_to_update
207
     * @param integer $allowed_to_delete
208
     * @return array
209
     */
210
    private function createUserJWT(
211
        int $id,
212
        string $login,
213
        int $pf_enabled,
214
        string $folders,
215
        string $items,
216
        string $keyTempo,
217
        string $sessionKey,
218
        int $admin,
219
        int $manager,
220
        int $can_create_root_folder,
221
        int $can_manage_all_users,
222
        string $roles,
223
        string $allowed_folders,
224
        int $allowed_to_create,
225
        int $allowed_to_read,
226
        int $allowed_to_update,
227
        int $allowed_to_delete,
228
    ): array
229
    {
230
        // Load config
231
        $configManager = new ConfigManager();
232
        $SETTINGS = $configManager->getAllSettings();
233
234
		$payload = [
235
            'username' => $login,
236
            'id' => $id,
237
            'exp' => (time() + $SETTINGS['api_token_duration'] + 600),
238
            'pf_enabled' => $pf_enabled,
239
            'folders_list' => $folders,
240
            'restricted_items_list' => $items,
241
            'key_tempo' => $keyTempo,
242
            'session_key' => $sessionKey, // Session key for decrypting private key from database
243
            'is_admin' => $admin,
244
            'is_manager' => $manager,
245
            'user_can_create_root_folder' => $can_create_root_folder,
246
            'user_can_manage_all_users' => $can_manage_all_users,
247
            'roles' => $roles,
248
            'allowed_folders' => $allowed_folders,
249
            'allowed_to_create' => $allowed_to_create,
250
            'allowed_to_read' => $allowed_to_read,
251
            'allowed_to_update' => $allowed_to_update,
252
            'allowed_to_delete' => $allowed_to_delete,
253
        ];
254
255
        return ['token' => JWT::encode($payload, DB_PASSWD, 'HS256')];
256
    }
257
258
    //end createUserJWT
259
260
261
    /**
262
     * Permit to build the list of folders the user can access
263
     *
264
     * @param array $userInfo
265
     * @return array
266
     */
267
    private function buildUserFoldersList(array $userInfo): array
268
    {
269
        //Build tree
270
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
271
        
272
        // Start by adding the manually added folders
273
        $allowedFolders = array_map('intval', explode(";", $userInfo['groupes_visibles']));
274
        $readOnlyFolders = [];
275
        $allowedFoldersByRoles = [];
276
        $restrictedFoldersForItems = [];
277
        $foldersLimited = [];
278
        $foldersLimitedFull = [];
279
        $restrictedItems = [];
280
        $personalFolders = [];
281
282
        $userFunctionId = explode(";", $userInfo['fonction_id']);
283
284
        // Get folders from the roles
285
        if (count($userFunctionId) > 0) {
286
            $rows = DB::query(
287
                'SELECT * 
288
                FROM ' . prefixTable('roles_values') . '
289
                WHERE role_id IN %li  AND type IN ("W", "ND", "NE", "NDNE", "R")',
290
                $userFunctionId
291
            );
292
            foreach ($rows as $record) {
293
                if ($record['type'] === 'R') {
294
                    array_push($readOnlyFolders, $record['folder_id']);
295
                } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
296
                    array_push($allowedFoldersByRoles, $record['folder_id']);
297
                }
298
            }
299
            $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
300
            $readOnlyFolders = array_unique($readOnlyFolders);
301
            // Clean arrays
302
            foreach ($allowedFoldersByRoles as $value) {
303
                $key = array_search($value, $readOnlyFolders);
304
                if ($key !== false) {
305
                    unset($readOnlyFolders[$key]);
306
                }
307
            }
308
        }
309
        
310
        // Does this user is allowed to see other items
311
        $inc = 0;
312
        $rows = DB::query(
313
            'SELECT id, id_tree 
314
            FROM ' . prefixTable('items') . '
315
            WHERE restricted_to LIKE %s'.
316
            (count($userFunctionId) > 0 ? ' AND id_tree NOT IN %li' : ''),
317
            $userInfo['id'],
318
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
319
        );
320
        foreach ($rows as $record) {
321
            // Exclude restriction on item if folder is fully accessible
322
            $restrictedFoldersForItems[$inc] = $record['id_tree'];
323
            ++$inc;
324
        }
325
326
        // Check for the users roles if some specific rights exist on items
327
        $rows = DB::query(
328
            'SELECT i.id_tree, r.item_id
329
            FROM ' . prefixTable('items') . ' AS i
330
            INNER JOIN ' . prefixTable('restriction_to_roles') . ' AS r ON (r.item_id=i.id)
331
            WHERE '.(count($userFunctionId) > 0 ? ' id_tree NOT IN %li AND ' : '').' i.id_tree != ""
332
            ORDER BY i.id_tree ASC',
333
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
334
        );
335
        foreach ($rows as $record) {
336
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
337
            //array_push($foldersLimitedFull, $record['item_id']);
338
            array_push($restrictedItems, $record['item_id']);
339
            array_push($foldersLimitedFull, $record['id_tree']);
340
            ++$inc;
341
        }
342
343
        // Add all personal folders
344
        $rows = DB::queryFirstRow(
345
            'SELECT id 
346
            FROM ' . prefixTable('nested_tree') . '
347
            WHERE title = %i AND personal_folder = 1'.
348
            (count($userFunctionId) > 0 ? ' AND id NOT IN %li' : ''),
349
            $userInfo['id'],
350
            count($userFunctionId) > 0 ? $userFunctionId : DB::sqleval('0')
351
        );
352
        if (empty($rows['id']) === false) {
353
            array_push($personalFolders, $rows['id']);
354
            // get all descendants
355
            $ids = $tree->getDescendants($rows['id'], false, false, true);
356
            foreach ($ids as $id) {
357
                array_push($personalFolders, $id);
358
            }
359
        }
360
361
        // All folders visibles
362
        return [
363
            'folders' => array_unique(
364
            array_filter(
365
                array_merge(
366
                    $allowedFolders,
367
                    $foldersLimitedFull,
368
                    $allowedFoldersByRoles,
369
                    $restrictedFoldersForItems,
370
                    $readOnlyFolders,
371
                    $personalFolders
372
                )
373
                )
374
            ),
375
            'items' => array_unique($restrictedItems),
376
        ];
377
    }
378
    //end buildUserFoldersList
379
}