Passed
Push — 1.0.x ( 8353d5...3a2c37 )
by Julien
21:28
created

Security::getRoles()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 2
eloc 5
c 1
b 1
f 0
nc 2
nop 0
dl 0
loc 8
ccs 6
cts 6
cp 1
crap 2
rs 10
1
<?php
2
3
/**
4
 * This file is part of the Zemit Framework.
5
 *
6
 * (c) Zemit Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zemit\Mvc\Model\Behavior;
13
14
use Phalcon\Di\Di;
15
use Phalcon\Messages\Message;
16
use Phalcon\Mvc\Model\Behavior;
17
use Phalcon\Mvc\ModelInterface;
18
use Phalcon\Acl\Adapter\AdapterInterface;
19
use Zemit\Mvc\Model\Behavior\Traits\ProgressTrait;
20
use Zemit\Mvc\Model\Behavior\Traits\SkippableTrait;
21
22
/**
23
 * The Security class provides methods for access control and permission checking.
24
 * This behavior will stop operations if the user is not allowed to run a certain type of action for the model
25
 */
26
class Security extends Behavior
27
{
28
    use SkippableTrait;
29
    use ProgressTrait;
30
    
31
    public static ?array $roles = null;
32
    public static ?AdapterInterface $acl = null;
33
    
34
    /**
35
     * Set the Access Control List (ACL) adapter.
36
     *
37
     * @param AdapterInterface|null $acl The ACL adapter to set. Defaults to null.
38
     * @return void
39
     */
40 1
    public static function setAcl(?AdapterInterface $acl = null): void
41
    {
42 1
        self::$acl = $acl;
43
    }
44
    
45
    /**
46
     * Get the Access Control List (ACL) with models and components elements
47
     *
48
     * @return AdapterInterface The ACL adapter instance
49
     */
50 2
    public static function getAcl(): AdapterInterface
51
    {
52 2
        if (is_null(self::$acl)) {
53 1
            $acl = Di::getDefault()->get('acl');
54 1
            assert($acl instanceof \Zemit\Acl\Acl);
55 1
            self::setAcl($acl->get(['models', 'components']));
56
        }
57 2
        assert(self::$acl instanceof AdapterInterface);
58 2
        return self::$acl;
59
    }
60
    
61
    /**
62
     * Set the roles
63
     *
64
     * @param array|null $roles The roles to set. Defaults to null.
65
     *
66
     * @return void
67
     */
68 1
    public static function setRoles(?array $roles = null): void
69
    {
70 1
        self::$roles = $roles;
71
    }
72
    
73
    /**
74
     * Get the roles of the current user
75
     *
76
     * This method retrieves the roles of the current user from the identity object. If the roles have not been
77
     * retrieved before, it retrieves them using the 'getAclRoles' method of the identity object. If the roles
78
     * have already been retrieved, it returns the cached roles. If the identity object is not found in the
79
     * DI container, an exception will be thrown.
80
     *
81
     * @return array The roles of the current user
82
     */
83 2
    public static function getRoles(): array
84
    {
85 2
        if (!isset(self::$roles)) {
86 1
            $identity = Di::getDefault()->get('identity');
87 1
            assert($identity instanceof \Zemit\Identity);
88 1
            self::setRoles($identity->getAclRoles());
89
        }
90 2
        return self::$roles ?? [];
91
    }
92
    
93
    /**
94
     * @param string $type The type of event to notify. Should be one of the following:
95
     *                     'beforeFind', 'beforeFindFirst', 'beforeCount', 'beforeSum', 'beforeAverage', 'beforeCreate', 
96
     *                     'beforeUpdate', 'beforeDelete', 'beforeRestore', 'beforeReorder'.
97
     * @param ModelInterface $model The model associated with the event.
98
     * 
99
     * @return bool|null Returns true if the event is allowed, false otherwise.
100
     *                   Returns null if the notification is disabled or if the check is skipped while in progress.
101
     */
102 2
    public function notify(string $type, ModelInterface $model): ?bool
103
    {
104 2
        if (!$this->isEnabled()) {
105
            return null;
106
        }
107
        
108
        // skip check while still in progress
109
        // needed to retrieve roles for itself
110 2
        if ($this->inProgress()) {
111
            return null;
112
        }
113
        
114 2
        $beforeEvents = [
115 2
            'beforeFind' => true,
116 2
            'beforeFindFirst' => true,
117 2
            'beforeCount' => true,
118 2
            'beforeSum' => true,
119 2
            'beforeAverage' => true,
120 2
            'beforeCreate' => true,
121 2
            'beforeUpdate' => true,
122 2
            'beforeDelete' => true,
123 2
            'beforeRestore' => true,
124 2
            'beforeReorder' => true,
125 2
        ];
126
        
127 2
        if ($beforeEvents[$type] ?? false) {
128 2
            self::staticStart();
129
            
130 2
            $type = (strpos($type, 'before') === 0) ? lcfirst(substr($type, 6)) : $type;
131 2
            $isAllowed = $this->isAllowed($type, $model);
132
            
133 2
            self::staticStop();
134 2
            return $isAllowed;
135
        }
136
        
137 2
        return true;
138
    }
139
    
140
    /**
141
     * Check if a specified type of operation is allowed on a model
142
     *
143
     * @param string $type The type of operation to check
144
     * @param ModelInterface $model The model to check permissions on
145
     * @param AdapterInterface|null $acl The ACL adapter to use for permission checks (optional, will default to the configured ACL if not provided)
146
     * @param array|null $roles The roles to check for permission (optional, will use the configured roles if not provided)
147
     * 
148
     * @return bool Returns true if the operation is allowed, false otherwise
149
     */
150 2
    public function isAllowed(string $type, ModelInterface $model, ?AdapterInterface $acl = null, ?array $roles = null): bool
151
    {
152 2
        $acl ??= self::getAcl();
153 2
        $modelClass = get_class($model);
154
        
155
        // component not found
156 2
        if (!$acl->isComponent($modelClass)) {
157
            $model->appendMessage(new Message(
158
                'Model permission not found for `' . $modelClass . '`',
159
                'id',
160
                'NotFound',
161
                404
162
            ));
163
            return false;
164
        }
165
        
166
        // allowed for roles
167 2
        $roles ??= self::getRoles();
168 2
        foreach ($roles as $role) {
169 2
            if ($acl->isAllowed($role, $modelClass, $type)) {
170 2
                return true;
171
            }
172
        }
173
        
174
        $model->appendMessage(new Message(
175
            'Current identity forbidden to execute `' . $type . '` on `' . $modelClass . '`',
176
            'id',
177
            'Forbidden',
178
            403
179
        ));
180
        return false;
181
    }
182
}
183