Passed
Push — develop ( d6f831...b3ddee )
by Nikolay
05:02
created

SecurityPlugin::notMatchedRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
/*
3
 * MikoPBX - free phone system for small business
4
 * Copyright (C) 2017-2020 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\Common\Models\AuthTokens;
23
use MikoPBX\Common\Providers\PBXConfModulesProvider;
24
use MikoPBX\Modules\Config\WebUIConfigInterface;
25
use Phalcon\Acl\Adapter\Memory as AclList;
26
use Phalcon\Acl\Component;
27
use Phalcon\Acl\Enum as AclEnum;
28
use Phalcon\Acl\Role as AclRole;
29
use Phalcon\Di\Injectable;
30
use Phalcon\Events\Event;
31
use Phalcon\Mvc\Dispatcher;
32
use Phalcon\Text;
33
34
/**
35
 * SecurityPlugin
36
 *
37
 * This is the security plugin which controls that users only have access to the modules they're assigned to
38
 */
39
class SecurityPlugin extends Injectable
40
{
41
42
    /**
43
     * Runs before dispatching a request.
44
     *
45
     * This method checks if the user is authenticated and authorized to access the requested controller and action. If the
46
     * user is not authenticated, the method redirects to the login page or returns a 403 response for AJAX requests. If the
47
     * requested controller does not exist, the method redirects to the extensions page. If the user is not authorized, the
48
     * method shows a 401 error page.
49
     *
50
     * @param Event $event The event object.
51
     * @param Dispatcher $dispatcher The dispatcher object.
52
     *
53
     * @return bool `true` if the request should continue, `false` otherwise.
54
     */
55
    public function beforeDispatch(/** @scrutinizer ignore-unused */ Event $event, Dispatcher $dispatcher): bool
56
    {
57
        // Check if user is authenticated
58
        $isLoggedIn = $this->checkUserAuth();
59
60
        // Get the controller and action names
61
        $controller = $dispatcher->getControllerName();
62
        $action = $dispatcher->getActionName();
63
64
        // Redirect to login page if user is not authenticated and the controller is not "session"
65
        if (!$isLoggedIn && strtoupper($controller) !== 'SESSION') {
66
            // Return a 403 response for AJAX requests
67
            if ($this->request->isAjax()) {
68
                $this->response->setStatusCode(403, 'Forbidden')->setContent('This user is not authorized')->send();
69
            } else {
70
                // Redirect to login page for normal requests
71
                $dispatcher->forward([
72
                    'controller' => 'session',
73
                    'action' => 'index',
74
                    'module' => 'admin-cabinet',
75
                    'namespace'=> 'MikoPBX\AdminCabinet\Controllers'
76
                ]);
77
            }
78
79
            return false;
80
        }
81
82
83
        // Check if the authenticated user is allowed to access the requested controller and action
84
        if ($isLoggedIn) {
85
            // Check if the desired controller exists or show the extensions page
86
            if (!$this->controllerExists($dispatcher)) {
87
                // Redirect to home page if controller does not set
88
                $homePath = $this->session->get('auth')['homePage'] ?? 'extensions/index';
89
                $controller = explode('/', $homePath)[0];
90
                $action = explode('/', $homePath)[1];
91
                $dispatcher->forward([
92
                        'controller' => $controller,
93
                        'action' => $action
94
                ]);
95
                return true;
96
            }
97
98
            if (!$this->isAllowedAction($controller, $action)) {
99
                // Show a 401 error if not allowed
100
                $dispatcher->forward([
101
                    'controller' => 'errors',
102
                    'action' => 'show401'
103
                ]);
104
                return true;
105
            }
106
        }
107
108
        return true;
109
    }
110
111
    /**
112
     *
113
     * Checks if the controller class exists.
114
     *
115
     * This method checks if the controller class exists by concatenating the controller name, namespace name, and handler
116
     * suffix obtained from the $dispatcher object. The method returns true if the class exists, false otherwise.
117
     *
118
     * @param Dispatcher $dispatcher The dispatcher object.
119
     * @return bool true if the controller class exists, false otherwise.
120
     */
121
    private
122
    function controllerExists(Dispatcher $dispatcher): bool
123
    {
124
        $controllerName = $dispatcher->getControllerName();
125
        $namespaceName = $dispatcher->getNamespaceName();
126
        $handlerSuffix = $dispatcher->getHandlerSuffix();
127
128
        return class_exists($namespaceName . '\\' . Text::camelize($controllerName) . $handlerSuffix);
129
    }
130
131
132
    /**
133
     * Checks if the current user is authenticated.
134
     *
135
     * This method checks if the current user is authenticated based on whether they have an existing session or a valid
136
     * "remember me" cookie.
137
     * If the request is from localhost or the user already has an active session, the method returns
138
     * true.
139
     * If a "remember me" cookie exists, the method checks if it matches any active tokens in the AuthTokens table.
140
     * If a match is found, the user's session is set, and the method returns true. If none of these conditions are met,
141
     * the method returns false.
142
     *
143
     * @return bool true if the user is authenticated, false otherwise.
144
     */
145
    private
146
    function checkUserAuth(): bool
147
    {
148
        // Check if it is a localhost request or if the user is already authenticated.
149
        if ($_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $this->session->has('auth')) {
150
            return true;
151
        }
152
153
        // Check if remember me cookie exists.
154
        if (!$this->cookies->has('random_token')) {
155
            return false;
156
        }
157
158
        $token = $this->cookies->get('random_token')->getValue();
159
        $currentDate = date("Y-m-d H:i:s", time());
160
161
        // Delete expired tokens and check if the token matches any active tokens.
162
        $userTokens = AuthTokens::find();
163
        foreach ($userTokens as $userToken) {
164
            if ($userToken->expiryDate < $currentDate) {
165
                $userToken->delete();
166
            } elseif ($this->security->checkHash($token, $userToken->tokenHash)) {
167
                $sessionParams = json_decode($userToken->sessionParams, true);
168
                $this->session->set('auth', $sessionParams);
169
                return true;
170
            }
171
        }
172
173
        return false;
174
    }
175
176
    /**
177
     * Gets the Access Control List (ACL).
178
     *
179
     * This method creates a new AclList object and sets the default action to AclEnum::DENY. It then adds two roles,
180
     * admins and guest, to the ACL, and sets the default permissions such that admins are allowed to perform any
181
     * action and guest is denied access to any action.
182
     *
183
     * Finally, it uses the PBXConfModulesProvider class to allow modules to modify the ACL, and returns the modified ACL.
184
     *
185
     * @return AclList The Access Control List.
186
     */
187
    public
188
    function getAcl(): AclList
189
    {
190
        $acl = new AclList();
191
        $acl->setDefaultAction(AclEnum::DENY);
192
193
        // Register roles
194
        $acl->addRole(new AclRole('admins', 'Admins'));
195
        $acl->addRole(new AclRole('guest', 'Guests'));
196
197
        // Default permissions
198
        $acl->allow('admins', '*', '*');
199
        $acl->deny('guest', '*', '*');
200
201
        // Modules HOOK
202
        PBXConfModulesProvider::hookModulesProcedure(WebUIConfigInterface::ON_AFTER_ACL_LIST_PREPARED, [&$acl]);
203
204
        // Allow to show ERROR controllers to everybody
205
        $acl->addComponent(new Component('Errors'), ['show401', 'show404', 'show500']);
206
        $acl->allow('*', 'Errors', ['show401', 'show404', 'show500']);
207
208
        // Allow to show session controllers actions to everybody
209
        $acl->addComponent(new Component('Session'), ['index', 'start', 'changeLanguage', 'end']);
210
        $acl->allow('*', 'Session', ['index', 'start', 'changeLanguage', 'end']);
211
212
        return $acl;
213
    }
214
215
    /**
216
     * Checks if an action is allowed for the current user.
217
     *
218
     * This method checks if the specified $action is allowed for the current user based on their role. It gets the user's
219
     * role from the session or sets it to 'guest' if no role is set. It then gets the Access Control List (ACL) and checks
220
     * if the $action is allowed for the current user's role. If the user is a guest or if the $action is not allowed,
221
     * the method returns false. Otherwise, it returns true.
222
     *
223
     * @param string $controller The name of the controller.
224
     * @param string $action The name of the action to check.
225
     * @return bool true if the action is allowed for the current user, false otherwise.
226
     */
227
    public
228
    function isAllowedAction(string $controller, string $action): bool
229
    {
230
        $role = $this->session->get('auth')['role'] ?? 'guest';
231
232
        if (strpos($controller, '_') > 0) {
233
            $controller = str_replace('_', '-', $controller);
234
        }
235
        $controller = Text::camelize($controller);
236
237
        $acl = $this->getAcl();
238
239
        $allowed = $acl->isAllowed($role, $controller, $action);
240
241
        if ($allowed != AclEnum::ALLOW) {
242
            return false;
243
        } else {
244
            return true;
245
        }
246
    }
247
248
}