Passed
Push — master ( fd5210...56a3aa )
by Nils
04:41
created

AuthModel::getUserAuth()   C

Complexity

Conditions 11
Paths 7

Size

Total Lines 89
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 55
c 1
b 0
f 0
nc 7
nop 3
dl 0
loc 89
rs 6.8351

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 library is distributed in the hope that it will be useful,
6
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
7
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8
 * ---
9
 *
10
 * @project   Teampass
11
 * @version    API
12
 *
13
 * @file      AuthModel.php
14
 * ---
15
 *
16
 * @author    Nils Laumaillé ([email protected])
17
 *
18
 * @copyright 2009-2024 Teampass.net
19
 *
20
 * @license   https://spdx.org/licenses/GPL-3.0-only.html#licenseText GPL-3.0
21
 * ---
22
 *
23
 * @see       https://www.teampass.net
24
 */
25
use TeampassClasses\PasswordManager\PasswordManager;
26
use TeampassClasses\NestedTree\NestedTree;
27
use Firebase\JWT\JWT;
28
use Firebase\JWT\Key;
29
30
require_once API_ROOT_PATH . "/Model/Database.php";
31
32
33
class AuthModel extends Database
34
{
35
36
37
    /**
38
     * Is the user allowed
39
     *
40
     * @param string $login
41
     * @param string $password
42
     * @param string $apikey
43
     * @return array
44
     */
45
    public function getUserAuth(string $login, string $password, string $apikey): array
46
    {
47
        // Sanitize
48
        include_once API_ROOT_PATH . '/../sources/main.functions.php';
49
        $inputData = dataSanitizer(
50
            [
51
                'login' => isset($login) === true ? $login : '',
52
                'password' => isset($password) === true ? $password : '',
53
                'apikey' => isset($apikey) === true ? $apikey : '',
54
            ],
55
            [
56
                'login' => 'trim|escape',
57
                'password' => 'trim|escape',
58
                'apikey' => 'trim|escape',
59
            ],
60
            API_ROOT_PATH . '/..'
0 ignored issues
show
Unused Code introduced by
The call to dataSanitizer() has too many arguments starting with API_ROOT_PATH . '/..'. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

60
        $inputData = /** @scrutinizer ignore-call */ dataSanitizer(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
61
        );
62
        if (empty($inputData['login']) === true || empty($inputData['apikey']) === true) {
63
            return ["error" => "Login failed.", "info" => "Empty entry"];
64
        }
65
        
66
        // Check apikey
67
        if (empty($inputData['password']) === true) {
68
            // case where it is a generic key
69
            $apiInfo = $this->select("SELECT count(*) FROM " . prefixTable('api') . " WHERE value='".$inputData['apikey']."' AND label='".$inputData['login']."'");
70
            if ((int) $apiInfo[0]['count(*)'] === 0) {
71
                return ["error" => "Login failed.", "info" => "apikey : Not valid"];
72
            }
73
74
            return ["error" => "Login failed.", "info" => "Not managed."];
75
        } else {
76
            // case where it is a user api key
77
            // Check if user exists
78
            $userInfoRes = $this->select(
79
                "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
80
                FROM " . prefixTable('users') . " AS u
81
                INNER JOIN " . prefixTable('api') . " AS a ON (a.user_id=u.id)
82
                WHERE login='".$inputData['login']."'");
83
            if (count($userInfoRes) === 0) {
84
                return ["error" => "Login failed.", "info" => "apikey : Not valid"];
85
            }
86
            $userInfoRes[0]['special'] = '';
87
            $userInfo = $userInfoRes[0];
88
            
89
            // Check password
90
            $passwordManager = new PasswordManager();
91
            if ($passwordManager->verifyPassword($userInfo['pw'], $inputData['password']) === true) {
92
                // Correct credentials
93
                // get user keys
94
                $privateKeyClear = decryptPrivateKey($inputData['password'], (string) $userInfo['private_key']);
95
96
                // check API key
97
                if ($inputData['apikey'] !== base64_decode(decryptUserObjectKey($userInfo['user_api_key'], $privateKeyClear))) {
98
                    return ["error" => "Login failed.", "apikey" => "Not valid"];
99
                }
100
101
                // Update user's key_tempo
102
                $keyTempo = bin2hex(random_bytes(16));
103
                $this->update(
104
                    "UPDATE " . prefixTable('users') . "
105
                    SET key_tempo='".$keyTempo."'
106
                    WHERE id=".$userInfo['id']
107
                );
108
                
109
                // get user folders list
110
                $ret = $this->buildUserFoldersList($userInfo);
111
112
                // Log user
113
                include API_ROOT_PATH . '/../includes/config/tp.config.php';
114
                logEvents($SETTINGS, 'api', 'user_connection', (string) $userInfo['id'], stripslashes($userInfo['login']));
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $SETTINGS seems to be never defined.
Loading history...
115
116
                // create JWT
117
                return $this->createUserJWT(
118
                    $userInfo['id'],
119
                    $inputData['login'],
120
                    $userInfo['personal_folder'],
121
                    $userInfo['public_key'],
122
                    $privateKeyClear,
123
                    implode(",", $ret['folders']),
124
                    implode(",", $ret['items']),
125
                    $keyTempo,
126
                    $userInfo['admin'],
127
                    $userInfo['gestionnaire'],
128
                    $userInfo['can_create_root_folder'],
129
                    $userInfo['can_manage_all_users'],
130
                    $userInfo['fonction_id'],
131
                );
132
            } else {
133
                return ["error" => "Login failed.", "info" => "password : Not valid"];
134
            }
135
        }
136
    }
137
    //end getUserAuth
138
139
    /**
140
     * Create a JWT
141
     *
142
     * @param integer $id
143
     * @param string $login
144
     * @param integer $pf_enabled
145
     * @param string $pubkey
146
     * @param string $privkey
147
     * @param string $folders
148
     * @param string $keyTempo
149
     * @param integer $admin
150
     * @param integer $manager
151
     * @param integer $can_create_root_folder
152
     * @param integer $can_manage_all_users
153
     * @param string $roles
154
     * @return array
155
     */
156
    private function createUserJWT(
157
        int $id,
158
        string $login,
159
        int $pf_enabled,
160
        string $pubkey,
161
        string $privkey,
162
        string $folders,
163
        string $items,
164
        string $keyTempo,
165
        int $admin,
166
        int $manager,
167
        int $can_create_root_folder,
168
        int $can_manage_all_users,
169
        string $roles
170
    ): array
171
    {
172
        include API_ROOT_PATH . '/../includes/config/tp.config.php';
173
        
174
		$payload = [
175
            'username' => $login,
176
            'id' => $id, 
177
            'exp' => (time() + $SETTINGS['api_token_duration'] + 600),
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $SETTINGS seems to be never defined.
Loading history...
178
            'public_key' => $pubkey,
179
            'private_key' => $privkey,
180
            'pf_enabled' => $pf_enabled,
181
            'folders_list' => $folders,
182
            'restricted_items_list' => $items,
183
            'key_tempo' => $keyTempo,
184
            'is_admin' => $admin,
185
            'is_manager' => $manager,
186
            'user_can_create_root_folder' => $can_create_root_folder,
187
            'user_can_manage_all_users' => $can_manage_all_users,
188
            'roles' => $roles,
189
        ];
190
        
191
        return ['token' => JWT::encode($payload, DB_PASSWD, 'HS256')];
192
    }
193
194
    //end createUserJWT
195
196
197
    /**
198
     * Permit to build the list of folders the user can access
199
     *
200
     * @param array $userInfo
201
     * @return array
202
     */
203
    private function buildUserFoldersList(array $userInfo): array
204
    {
205
        //Build tree
206
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
207
        
208
        // Start by adding the manually added folders
209
        $allowedFolders = array_map('intval', explode(";", $userInfo['groupes_visibles']));
210
        $readOnlyFolders = [];
211
        $allowedFoldersByRoles = [];
212
        $restrictedFoldersForItems = [];
213
        $foldersLimited = [];
214
        $foldersLimitedFull = [];
215
        $restrictedItems = [];
216
        $personalFolders = [];
217
218
        $userFunctionId = str_replace(";", ",", $userInfo['fonction_id']);
219
220
        // Get folders from the roles
221
        if (empty($userFunctionId) === false) {
222
            $rows = $this->select("SELECT * FROM " . prefixTable('roles_values') . " WHERE role_id IN (".$userFunctionId.") AND type IN ('W', 'ND', 'NE', 'NDNE', 'R')");
223
            foreach ($rows as $record) {
224
                if ($record['type'] === 'R') {
225
                    array_push($readOnlyFolders, $record['folder_id']);
226
                } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
227
                    array_push($allowedFoldersByRoles, $record['folder_id']);
228
                }
229
            }
230
            $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
231
            $readOnlyFolders = array_unique($readOnlyFolders);
232
            // Clean arrays
233
            foreach ($allowedFoldersByRoles as $value) {
234
                $key = array_search($value, $readOnlyFolders);
235
                if ($key !== false) {
236
                    unset($readOnlyFolders[$key]);
237
                }
238
            }
239
        }
240
        
241
        // Does this user is allowed to see other items
242
        $inc = 0;
243
        $rows = $this->select("SELECT id, id_tree FROM " . prefixTable('items') . " WHERE restricted_to LIKE '".$userInfo['id']."'".
244
            (empty($userFunctionId) === false ? ' AND id_tree NOT IN ('.$userFunctionId.')' : ''));
245
        foreach ($rows as $record) {
246
            // Exclude restriction on item if folder is fully accessible
247
            $restrictedFoldersForItems[$inc] = $record['id_tree'];
248
            ++$inc;
249
        }
250
251
        // Check for the users roles if some specific rights exist on items
252
        $rows = $this->select("SELECT i.id_tree, r.item_id
253
            FROM " . prefixTable('items') . " as i
254
            INNER JOIN " . prefixTable('restriction_to_roles') . " as r ON (r.item_id=i.id)
255
            WHERE ".(empty($userFunctionId) === false ? ' id_tree NOT IN ('.$userFunctionId.') AND ' : '')." i.id_tree != ''
256
            ORDER BY i.id_tree ASC");
257
        foreach ($rows as $record) {
258
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
259
            //array_push($foldersLimitedFull, $record['item_id']);
260
            array_push($restrictedItems, $record['item_id']);
261
            array_push($foldersLimitedFull, $record['id_tree']);
262
            ++$inc;
263
        }
264
265
        // Add all personal folders
266
        $rows = $this->select(
267
            'SELECT id
268
            FROM ' . prefixTable('nested_tree') . '
269
            WHERE title = '.$userInfo['id'].' AND personal_folder = 1'.
270
            (empty($userFunctionId) === false ? ' AND id NOT IN ('.$userFunctionId.')' : '').
271
            ' LIMIT 0,1'
272
        );
273
        if (empty($rows['id']) === false) {
274
            array_push($personalFolders, $rows['id']);
275
            // get all descendants
276
            $ids = $tree->getDescendants($rows['id'], false, false, true);
277
            foreach ($ids as $id) {
278
                array_push($personalFolders, $id);
279
            }
280
        }
281
282
        // All folders visibles
283
        return [
284
            'folders' => array_unique(
285
            array_filter(
286
                array_merge(
287
                    $allowedFolders,
288
                    $foldersLimitedFull,
289
                    $allowedFoldersByRoles,
290
                    $restrictedFoldersForItems,
291
                    $readOnlyFolders,
292
                    $personalFolders
293
                )
294
                )
295
            ),
296
            'items' => array_unique($restrictedItems),
297
        ];
298
    }
299
    //end buildUserFoldersList
300
}