Test Failed
Push — master ( ce60e5...378563 )
by Julien
12:41 queued 07:49
created

Security::notify()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 36
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5.6205

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 36
ccs 17
cts 24
cp 0.7083
rs 9.2568
cc 5
nc 5
nop 2
crap 5.6205
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\Acl\Adapter\Memory;
15
use Phalcon\Di\Di;
16
use Phalcon\Messages\Message;
17
use Phalcon\Mvc\Model\Behavior;
18
use Phalcon\Mvc\ModelInterface;
19
20
/**
21
 * Allows to check if the current identity is allowed to run some model actions
22
 * this behavior will stop operations if not allowed
23
 */
24
class Security extends Behavior
25
{
26
    use SkippableTrait;
27
    use ProgressTrait;
28
    
29
    public static ?array $roles = null;
30
    
31
    public static ?Memory $acl = null;
32
    
33
    /**
34
     * Set the ACL
35
     */
36
    public static function setAcl(?Memory $acl = null): void
37
    {
38
        self::$acl = $acl;
39
    }
40
    
41
    /**
42
     * Get the ACL
43
     */
44
    public static function getAcl(): Memory
45
    {
46
        if (is_null(self::$acl)) {
47
            $acl = Di::getDefault()->get('acl');
48
            assert($acl instanceof \Zemit\Acl\Acl);
49
            self::setAcl($acl->get(['models', 'components']));
50
        }
51
        return self::$acl;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::acl could return the type null which is incompatible with the type-hinted return Phalcon\Acl\Adapter\Memory. Consider adding an additional type-check to rule them out.
Loading history...
52
    }
53
    
54
    /**
55
     * Set the current identity's roles
56
     */
57
    public static function setRoles(?array $roles = null): void
58
    {
59
        self::$roles = $roles;
60
    }
61
    
62
    /**
63
     * Return the current identity's acl roles
64
     */
65
    public static function getRoles(): array
66
    {
67
        if (is_null(self::$roles)) {
68
            $identity = Di::getDefault()->get('identity');
69
            assert($identity instanceof \Zemit\Identity);
70
            self::setRoles($identity->getAclRoles());
71
        }
72
        return self::$roles;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::roles could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
73
    }
74
    
75 2
    public function __construct(?array $options = null)
76
    {
77 2
        parent::__construct($options);
0 ignored issues
show
Bug introduced by
It seems like $options can also be of type null; however, parameter $options of Phalcon\Mvc\Model\Behavior::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

77
        parent::__construct(/** @scrutinizer ignore-type */ $options);
Loading history...
78
    }
79
    
80
    /**
81
     * Handling security (acl) on before model's events
82
     */
83 1
    public function notify(string $type, ModelInterface $model): ?bool
84
    {
85 1
        if (!$this->isEnabled()) {
86
            return null;
87
        }
88
        
89
        // skip check while still in progress
90
        // needed to retrieve roles for itself
91 1
        if ($this->inProgress()) {
92
            return null;
93
        }
94
        
95 1
        $beforeEvents = [
96 1
            'beforeFind' => true,
97 1
            'beforeFindFirst' => true,
98 1
            'beforeCount' => true,
99 1
            'beforeSum' => true,
100 1
            'beforeAverage' => true,
101 1
            'beforeCreate' => true,
102 1
            'beforeUpdate' => true,
103 1
            'beforeDelete' => true,
104 1
            'beforeRestore' => true,
105 1
            'beforeReorder' => true,
106 1
        ];
107
        
108 1
        if ($beforeEvents[$type] ?? false) {
109
            self::staticStart();
110
            
111
            $type = (strpos($type, 'before') === 0) ? lcfirst(substr($type, 6)) : $type;
112
            $isAllowed = $this->isAllowed($type, $model);
113
            
114
            self::staticStop();
115
            return $isAllowed;
116
        }
117
        
118 1
        return true;
119
    }
120
    
121
    public function isAllowed(string $type, ModelInterface $model, ?Memory $acl = null, ?array $roles = null): bool
122
    {
123
        $acl ??= self::getAcl();
124
        $modelClass = get_class($model);
125
        
126
        // component not found
127
        if (!$acl->isComponent($modelClass)) {
128
            $model->appendMessage(new Message(
129
                'Model permission not found for `' . $modelClass . '`',
130
                'id',
131
                'NotFound',
132
                404
133
            ));
134
            return false;
135
        }
136
        
137
        // allowed for roles
138
        $roles ??= self::getRoles();
139
        foreach ($roles as $role) {
140
            if ($acl->isAllowed($role, $modelClass, $type)) {
141
                return true;
142
            }
143
        }
144
        
145
        $model->appendMessage(new Message(
146
            'Current identity forbidden to execute `' . $type . '` on `' . $modelClass . '`',
147
            'id',
148
            'Forbidden',
149
            403
150
        ));
151
        return false;
152
    }
153
}
154