Passed
Push — develop ( a4a90f...fc5c50 )
by Nikolay
05:38
created

SecurityPlugin   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 150
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 17
eloc 64
dl 0
loc 150
rs 10
c 1
b 0
f 0

3 Methods

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