Passed
Push — develop ( 04a2ba...44a5a9 )
by Nikolay
04:26
created

SecurityPlugin::checkUserAuth()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 29
rs 9.1111
cc 6
nc 6
nop 0
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright © 2017-2023 Alexey Portnov and Nikolay Beketov
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program 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 along with this program.
17
 * If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
namespace MikoPBX\AdminCabinet\Plugins;
21
22
use MikoPBX\AdminCabinet\Controllers\SessionController;
23
use MikoPBX\Common\Models\AuthTokens;
24
use MikoPBX\Common\Providers\AclProvider;
25
use Phalcon\Acl\Enum as AclEnum;
26
use Phalcon\Di\Injectable;
27
use Phalcon\Events\Event;
28
use Phalcon\Mvc\Dispatcher;
29
use Phalcon\Text;
30
31
/**
32
 * SecurityPlugin
33
 *
34
 * This is the security plugin which controls that users only have access to the modules they're assigned to
35
 */
36
class SecurityPlugin extends Injectable
37
{
38
39
    /**
40
     * Runs before dispatching a request.
41
     *
42
     * This method checks if the user is authenticated and authorized to access the requested controller and action. If the
43
     * user is not authenticated, the method redirects to the login page or returns a 403 response for AJAX requests. If the
44
     * requested controller does not exist, the method redirects to the extensions page. If the user is not authorized, the
45
     * method shows a 401 error page.
46
     *
47
     * @param Event $event The event object.
48
     * @param Dispatcher $dispatcher The dispatcher object.
49
     *
50
     * @return bool `true` if the request should continue, `false` otherwise.
51
     */
52
    public function beforeDispatch(/** @scrutinizer ignore-unused */ Event $event, Dispatcher $dispatcher): bool
53
    {
54
        // Check if user is authenticated
55
        $isAuthenticated = $this->checkUserAuth() || $this->isLocalHostRequest();
56
57
        // Get the controller and action names
58
        $controller = $dispatcher->getControllerName();
59
        $action = $dispatcher->getActionName();
60
61
        // Redirect to login page if user is not authenticated and the controller is not "session"
62
        if (!$isAuthenticated && strtoupper($controller) !== 'SESSION') {
63
            // Return a 403 response for AJAX requests
64
            if ($this->request->isAjax()) {
65
                $this->response->setStatusCode(403, 'Forbidden')->setContent('This user is not authorized')->send();
66
            } else {
67
                // Redirect to login page for normal requests
68
                $dispatcher->forward([
69
                    'controller' => 'session',
70
                    'action' => 'index',
71
                    'module' => 'admin-cabinet',
72
                    'namespace' => 'MikoPBX\AdminCabinet\Controllers'
73
                ]);
74
            }
75
76
            return false;
77
        }
78
79
        // Check if the authenticated user is allowed to access the requested controller and action
80
        if ($isAuthenticated) {
81
            // Check if the desired controller exists or show the extensions page
82
            $controllerClass = $this->dispatcher->getHandlerClass();
0 ignored issues
show
Bug introduced by
The method getHandlerClass() does not exist on Phalcon\Mvc\DispatcherInterface. Did you maybe mean getHandlerSuffix()? ( Ignorable by Annotation )

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

82
            /** @scrutinizer ignore-call */ 
83
            $controllerClass = $this->dispatcher->getHandlerClass();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
83
            if (!class_exists($controllerClass)
84
                || (strtoupper($controller) === 'SESSION' && strtoupper($action) !== 'END')) {
85
                // Redirect to home page if controller does not set or user logged in but still on session page
86
                $homePath = $this->session->get(SessionController::SESSION_ID)[SessionController::HOME_PAGE];
87
                if (empty($homePath)){
88
                    $dispatcher->forward([
89
                        'module' => 'admin-cabinet',
90
                        'controller' => 'errors',
91
                        'action' => 'show404',
92
                        'namespace' => 'MikoPBX\AdminCabinet\Controllers'
93
                    ]);
94
                    return true;
95
                }
96
                $module = explode('/', $homePath)[1];
97
                $controller = explode('/', $homePath)[2];
98
                $action = explode('/', $homePath)[3];
99
                $dispatcher->forward([
100
                    'module' => $module,
101
                    'controller' => $controller,
102
                    'action' => $action
103
                ]);
104
                return true;
105
            }
106
            if (!$this->isLocalHostRequest()
107
                && !$this->isAllowedAction($controllerClass, $action)
108
                && !in_array(strtoupper($controller), ['ERRORS','SESSION'])
109
            ) {
110
                // Show a 401 error if not allowed
111
                $dispatcher->forward([
112
                    'module' => 'admin-cabinet',
113
                    'controller' => 'errors',
114
                    'action' => 'show401',
115
                    'namespace' => 'MikoPBX\AdminCabinet\Controllers'
116
                ]);
117
                return true;
118
            }
119
        }
120
121
        return true;
122
    }
123
124
125
    /**
126
     * Checks if the current user is authenticated.
127
     *
128
     * This method checks if the current user is authenticated based on whether they have an existing session or a valid
129
     * "remember me" cookie.
130
     * If the request is from localhost or the user already has an active session, the method returns
131
     * true.
132
     * If a "remember me" cookie exists, the method checks if it matches any active tokens in the AuthTokens table.
133
     * If a match is found, the user's session is set, and the method returns true. If none of these conditions are met,
134
     * the method returns false.
135
     *
136
     * @return bool true if the user is authenticated, false otherwise.
137
     */
138
    private
139
    function checkUserAuth(): bool
140
    {
141
        // Check if it is a localhost request or if the user is already authenticated.
142
        if ($this->session->has(SessionController::SESSION_ID)) {
143
            return true;
144
        }
145
146
        // Check if remember me cookie exists.
147
        if (!$this->cookies->has('random_token')) {
148
            return false;
149
        }
150
151
        $token = $this->cookies->get('random_token')->getValue();
152
        $currentDate = date("Y-m-d H:i:s", time());
153
154
        // Delete expired tokens and check if the token matches any active tokens.
155
        $userTokens = AuthTokens::find();
156
        foreach ($userTokens as $userToken) {
157
            if ($userToken->expiryDate < $currentDate) {
158
                $userToken->delete();
159
            } elseif ($this->security->checkHash($token, $userToken->tokenHash)) {
160
                $sessionParams = json_decode($userToken->sessionParams, true);
161
                $this->session->set(SessionController::SESSION_ID, $sessionParams);
162
                return true;
163
            }
164
        }
165
166
        return false;
167
    }
168
169
    /**
170
     * Checks if an action is allowed for the current user.
171
     *
172
     * This method checks if the specified $action is allowed for the current user based on their role. It gets the user's
173
     * role from the session or sets it to 'guests' if no role is set. It then gets the Access Control List (ACL) and checks
174
     * if the $action is allowed for the current user's role. If the user is a guest or if the $action is not allowed,
175
     * the method returns false. Otherwise, it returns true.
176
     *
177
     * @param string $controller The full name of the controller class.
178
     * @param string $action The name of the action to check.
179
     * @return bool true if the action is allowed for the current user, false otherwise.
180
     */
181
    public function isAllowedAction(string $controller, string $action): bool
182
    {
183
        $role = $this->session->get(SessionController::SESSION_ID)[SessionController::ROLE] ?? AclProvider::ROLE_GUESTS;
184
        $acl = $this->di->get(AclProvider::SERVICE_NAME);
185
        $allowed = $acl->isAllowed($role, $controller, $action);
186
        if ($allowed != AclEnum::ALLOW) {
187
            return false;
188
        } else {
189
            return true;
190
        }
191
    }
192
193
    /**
194
     * Check if the request is coming from localhost.
195
     *
196
     * @return bool
197
     */
198
    public function isLocalHostRequest(): bool
199
    {
200
        return ($_SERVER['REMOTE_ADDR'] === '127.0.0.1');
201
    }
202
203
}