Passed
Push — master ( 3bc500...977fa1 )
by Jan
04:20
created

ElementPermissionListener::postLoadHandler()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 6
eloc 14
c 5
b 0
f 0
nc 5
nop 2
dl 0
loc 31
rs 9.2222
1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 2
10
 * of the License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
20
 */
21
22
namespace App\Security\EntityListeners;
23
24
use App\Entity\Base\DBElement;
25
use App\Entity\UserSystem\User;
26
use App\Security\Annotations\ColumnSecurity;
27
use Doctrine\Common\Annotations\Reader;
28
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
29
use Doctrine\ORM\EntityManagerInterface;
30
use Doctrine\ORM\Event\PreFlushEventArgs;
31
use Doctrine\ORM\Mapping as ORM;
32
use Doctrine\ORM\Mapping\PostLoad;
33
use Doctrine\ORM\Mapping\PreUpdate;
34
use ReflectionClass;
35
use Symfony\Component\HttpKernel\KernelInterface;
36
use Symfony\Component\Security\Core\Security;
37
38
/**
39
 * The purpose of this class is to hook into the doctrine entity lifecycle and restrict access to entity informations
40
 * configured by ColoumnSecurity Annotation.
41
 * If the current programm is running from CLI (like a CLI command), the security checks are disabled.
42
 * (Commands should be able to do everything they like).
43
 *
44
 * If a user does not have access to an coloumn, it will be filled, with a placeholder, after doctrine loading is finished.
45
 * The edit process is also catched, so that these placeholders, does not get saved to database.
46
 */
47
class ElementPermissionListener
48
{
49
    protected $security;
50
    protected $reader;
51
    protected $em;
52
    protected $disabled;
53
54
    protected $perm_cache;
55
56
    public function __construct(Security $security, Reader $reader, EntityManagerInterface $em, KernelInterface $kernel)
0 ignored issues
show
Unused Code introduced by
The parameter $kernel is not used and could be removed. ( Ignorable by Annotation )

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

56
    public function __construct(Security $security, Reader $reader, EntityManagerInterface $em, /** @scrutinizer ignore-unused */ KernelInterface $kernel)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
57
    {
58
        $this->security = $security;
59
        $this->reader = $reader;
60
        $this->em = $em;
61
        //Disable security when the current program is running from CLI
62
        $this->disabled = $this->isRunningFromCLI();
63
        $this->perm_cache = [];
64
    }
65
66
    /**
67
     * This function checks if the current script is run from web or from a terminal.
68
     *
69
     * @return bool Returns true if the current programm is running from CLI (terminal)
70
     */
71
    protected function isRunningFromCLI()
72
    {
73
        if (empty($_SERVER['REMOTE_ADDR']) && !isset($_SERVER['HTTP_USER_AGENT']) && \count($_SERVER['argv']) > 0) {
74
            return true;
75
        }
76
77
        return false;
78
    }
79
80
    /**
81
     * Checks if access to the property of the given element is granted.
82
     * This function adds an additional cache layer, where the voters are called only once (to improve performance).
83
     * @param string $mode What operation should be checked. Must be 'read' or 'edit'
84
     * @param ColumnSecurity $annotation The annotation of the property that should be checked
85
     * @param DBElement $element The element that should for which should be checked
86
     * @return bool True if the user is allowed to read that property
87
     */
88
    protected function isGranted(string $mode, ColumnSecurity $annotation, DBElement $element) : bool
89
    {
90
        if ($mode === 'read') {
91
            $operation = $annotation->getReadOperationName();
92
        } elseif ($mode === 'edit') {
93
            $operation = $annotation->getEditOperationName();
94
        } else {
95
            throw new \InvalidArgumentException('$mode must be either "read" or "edit"!');
96
        }
97
98
        //Users must always be checked, because its return value can differ if it is the user itself or something else
99
        if ($element instanceof User) {
100
            return $this->security->isGranted($operation, $element);
101
        }
102
103
        //Check if we have already have saved the permission, otherwise save it to cache
104
        if (!isset($this->perm_cache[$mode][get_class($element)][$operation])) {
105
            $this->perm_cache[$mode][get_class($element)][$operation] = $this->security->isGranted($operation, $element);
106
        }
107
108
        return $this->perm_cache[$mode][get_class($element)][$operation];
109
110
    }
111
112
    /**
113
     * @PostLoad
114
     * @ORM\PostUpdate()
115
     * This function is called after doctrine filled, the entity properties with db values.
116
     * We use this, to check if the user is allowed to access these properties, and if not, we write a placeholder
117
     * into the element properties, so that a user only gets non sensitve data.
118
     *
119
     * This function is also called after an entity was updated, so we dont show the original data to user,
120
     * after an update.
121
     */
122
    public function postLoadHandler(DBElement $element, LifecycleEventArgs $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

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

122
    public function postLoadHandler(DBElement $element, /** @scrutinizer ignore-unused */ LifecycleEventArgs $event)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
123
    {
124
        //Do nothing if security is disabled
125
        if ($this->disabled) {
126
            return;
127
        }
128
129
        //Read Annotations and properties.
130
        $reflectionClass = new ReflectionClass($element);
131
        $properties = $reflectionClass->getProperties();
132
133
        foreach ($properties as $property) {
134
            /**
135
             * @var ColumnSecurity
136
             */
137
            $annotation = $this->reader->getPropertyAnnotation(
138
                $property,
139
                ColumnSecurity::class
140
            );
141
142
            //Check if user is allowed to read info, otherwise apply placeholder
143
            if ((null !== $annotation) && !$this->isGranted('read', $annotation, $element)) {
144
                $property->setAccessible(true);
145
                $value = $annotation->getPlaceholder();
146
147
                //Detach placeholder entities, so we dont get cascade errors
148
                if ($value instanceof DBElement) {
149
                    $this->em->detach($value);
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\Common\Persiste...ObjectManager::detach() has been deprecated: Detach operation is deprecated and will be removed in Persistence 2.0. Please use {@see ObjectManager::clear()} instead. ( Ignorable by Annotation )

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

149
                    /** @scrutinizer ignore-deprecated */ $this->em->detach($value);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
150
                }
151
152
                $property->setValue($element, $value);
153
            }
154
        }
155
    }
156
157
    /**
158
     * @ORM\PreFlush()
159
     * This function is called before flushing. We use it, to remove all placeholders.
160
     * We do it here and not in preupdate, because this is called before calculating the changeset,
161
     * and so we dont get problems with orphan removal.
162
     */
163
    public function preFlushHandler(DBElement $element, PreFlushEventArgs $eventArgs)
164
    {
165
        //Do nothing if security is disabled
166
        if ($this->disabled) {
167
            return;
168
        }
169
170
        $em = $eventArgs->getEntityManager();
0 ignored issues
show
Unused Code introduced by
The assignment to $em is dead and can be removed.
Loading history...
171
        $unitOfWork = $eventArgs->getEntityManager()->getUnitOfWork();
172
173
        $reflectionClass = new ReflectionClass($element);
174
        $properties = $reflectionClass->getProperties();
175
176
        $old_data = $unitOfWork->getOriginalEntityData($element);
177
178
        foreach ($properties as $property) {
179
            $annotation = $this->reader->getPropertyAnnotation(
180
                $property,
181
                ColumnSecurity::class
182
            );
183
184
            $changed = false;
185
186
            //Only set the field if it has an annotation
187
            if (null !== $annotation) {
188
                $property->setAccessible(true);
189
190
                //If the user is not allowed to edit or read this property, reset all values.
191
                if ((!$this->isGranted('read', $annotation, $element)
192
                    || !$this->isGranted('edit', $annotation->getReadOperationName(), $element))) {
193
                    //Set value to old value, so that there a no change to this property
194
                    if (isset($old_data[$property->getName()])) {
195
                        $property->setValue($element, $old_data[$property->getName()]);
196
                        $changed = true;
197
                    }
198
                }
199
200
                if ($changed) {
201
                    //Schedule for update, so the post update method will be called
202
                    $unitOfWork->scheduleForUpdate($element);
203
                }
204
            }
205
        }
206
    }
207
}
208