Passed
Push — develop ( 5ea0eb...95a6ec )
by Nikolay
04:45
created

SecurityPlugin::getAcl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
dl 0
loc 26
rs 9.8333
c 1
b 0
f 0
cc 1
nc 1
nop 0
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\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
            if (!$this->controllerExists($dispatcher)) {
83
                // Redirect to home page if controller does not set
84
                $homePath = $this->session->get(SessionController::SESSION_ID)['homePage'] ?? 'extensions/index';
85
                $controller = explode('/', $homePath)[0];
86
                $action = explode('/', $homePath)[1];
87
                $dispatcher->forward([
88
                    'controller' => $controller,
89
                    'action' => $action
90
                ]);
91
                return true;
92
            }
93
94
            if (!$this->isAllowedAction($controller, $action)) {
95
                // Show a 401 error if not allowed
96
                $dispatcher->forward([
97
                    'controller' => 'errors',
98
                    'action' => 'show401'
99
                ]);
100
                return true;
101
            }
102
        }
103
104
        return true;
105
    }
106
107
    /**
108
     *
109
     * Checks if the controller class exists.
110
     *
111
     * This method checks if the controller class exists by concatenating the controller name, namespace name, and handler
112
     * suffix obtained from the $dispatcher object. The method returns true if the class exists, false otherwise.
113
     *
114
     * @param Dispatcher $dispatcher The dispatcher object.
115
     * @return bool true if the controller class exists, false otherwise.
116
     */
117
    private
118
    function controllerExists(Dispatcher $dispatcher): bool
119
    {
120
        $controllerName = $dispatcher->getControllerName();
121
        $namespaceName = $dispatcher->getNamespaceName();
122
        $handlerSuffix = $dispatcher->getHandlerSuffix();
123
124
        return class_exists($namespaceName . '\\' . Text::camelize($controllerName) . $handlerSuffix);
125
    }
126
127
128
    /**
129
     * Checks if the current user is authenticated.
130
     *
131
     * This method checks if the current user is authenticated based on whether they have an existing session or a valid
132
     * "remember me" cookie.
133
     * If the request is from localhost or the user already has an active session, the method returns
134
     * true.
135
     * If a "remember me" cookie exists, the method checks if it matches any active tokens in the AuthTokens table.
136
     * If a match is found, the user's session is set, and the method returns true. If none of these conditions are met,
137
     * the method returns false.
138
     *
139
     * @return bool true if the user is authenticated, false otherwise.
140
     */
141
    private
142
    function checkUserAuth(): bool
143
    {
144
        // Check if it is a localhost request or if the user is already authenticated.
145
        if ($_SERVER['REMOTE_ADDR'] === '127.0.0.1' || $this->session->has(SessionController::SESSION_ID)) {
146
            return true;
147
        }
148
149
        // Check if remember me cookie exists.
150
        if (!$this->cookies->has('random_token')) {
151
            return false;
152
        }
153
154
        $token = $this->cookies->get('random_token')->getValue();
155
        $currentDate = date("Y-m-d H:i:s", time());
156
157
        // Delete expired tokens and check if the token matches any active tokens.
158
        $userTokens = AuthTokens::find();
159
        foreach ($userTokens as $userToken) {
160
            if ($userToken->expiryDate < $currentDate) {
161
                $userToken->delete();
162
            } elseif ($this->security->checkHash($token, $userToken->tokenHash)) {
163
                $sessionParams = json_decode($userToken->sessionParams, true);
164
                $this->session->set(SessionController::SESSION_ID, $sessionParams);
165
                return true;
166
            }
167
        }
168
169
        return false;
170
    }
171
172
    /**
173
     * Checks if an action is allowed for the current user.
174
     *
175
     * This method checks if the specified $action is allowed for the current user based on their role. It gets the user's
176
     * role from the session or sets it to 'guest' if no role is set. It then gets the Access Control List (ACL) and checks
177
     * if the $action is allowed for the current user's role. If the user is a guest or if the $action is not allowed,
178
     * the method returns false. Otherwise, it returns true.
179
     *
180
     * @param string $controller The name of the controller.
181
     * @param string $action The name of the action to check.
182
     * @return bool true if the action is allowed for the current user, false otherwise.
183
     */
184
    public function isAllowedAction(string $controller, string $action): bool
185
    {
186
        $role = $this->session->get(SessionController::SESSION_ID)['role'] ?? 'guest';
187
188
        if (strpos($controller, '_') > 0) {
189
            $controller = str_replace('_', '-', $controller);
190
        }
191
        $controller = Text::camelize($controller);
192
193
        $acl = $this->di->get(AclProvider::SERVICE_NAME);
194
195
        $allowed = $acl->isAllowed($role, $controller, $action);
196
197
        if ($allowed != AclEnum::ALLOW) {
198
            return false;
199
        } else {
200
            return true;
201
        }
202
    }
203
204
}