3 * @see https://github.com/zendframework/zend-permissions-acl for the canonical source repository
4 * @copyright Copyright (c) 2018 Zend Technologies USA Inc. (https://www.zend.com)
5 * @license https://github.com/zendframework/zend-permissions-acl/blob/master/LICENSE.md New BSD License
8 namespace Zend\Permissions\Acl\Assertion
;
10 use ReflectionProperty
;
11 use Zend\Permissions\Acl\Acl
;
12 use Zend\Permissions\Acl\Role\RoleInterface
;
13 use Zend\Permissions\Acl\
Resource\ResourceInterface
;
14 use Zend\Permissions\Acl\Assertion\Exception\InvalidAssertionException
;
15 use Zend\Permissions\Acl\Exception\RuntimeException
;
18 * Create an assertion based on expression rules.
20 * Each of the constructor, fromProperties, and fromArray methods allow you to
21 * define expression rules, and these include the left hand side, operator, and
22 * right hand side of the expression.
24 * The left and right hand sides of the expression are the values to compare.
25 * These values can be either an exact value to match, or an array with the key
26 * OPERAND_CONTEXT_PROPERTY pointing to one of two value types.
28 * First, it can be a string value matching one of "acl", "privilege", "role",
29 * or "resource", with the latter two values being the most common. In those
30 * cases, the matching value passed to `assert()` will be used in the
33 * Second, it can be a dot-separated property path string of the format
34 * "object.property", representing the associated object (role, resource, acl,
35 * or privilege) and its property to test against. The property may refer to a
36 * public property, or a public `get` or `is` method (following
37 * canonicalization of the property by replacing underscore separated values
38 * with camelCase values).
40 final class ExpressionAssertion
implements AssertionInterface
42 const OPERAND_CONTEXT_PROPERTY
= '__context';
44 const OPERATOR_EQ
= '=';
45 const OPERATOR_NEQ
= '!=';
46 const OPERATOR_LT
= '<';
47 const OPERATOR_LTE
= '<=';
48 const OPERATOR_GT
= '>';
49 const OPERATOR_GTE
= '>=';
50 const OPERATOR_IN
= 'in';
51 const OPERATOR_NIN
= '!in';
52 const OPERATOR_REGEX
= 'regex';
53 const OPERATOR_NREGEX
= '!regex';
54 const OPERATOR_SAME
= '===';
55 const OPERATOR_NSAME
= '!==';
60 private static $validOperators = [
70 self
::OPERATOR_NREGEX
,
93 * Note that the constructor is marked private; use `fromProperties()` or
94 * `fromArray()` to create an instance.
96 * @param mixed|array $left See the class description for valid values.
97 * @param string $operator One of the OPERATOR constants (or their values)
98 * @param mixed|array $right See the class description for valid values.
100 private function __construct($left, $operator, $right)
103 $this->operator
= $operator;
104 $this->right
= $right;
108 * @param mixed|array $left See the class description for valid values.
109 * @param string $operator One of the OPERATOR constants (or their values)
110 * @param mixed|array $right See the class description for valid values.
112 * @throws InvalidAssertionException if either operand is invalid.
113 * @throws InvalidAssertionException if the operator is not supported.
115 public static function fromProperties($left, $operator, $right)
117 $operator = strtolower($operator);
119 self
::validateOperand($left);
120 self
::validateOperator($operator);
121 self
::validateOperand($right);
123 return new self($left, $operator, $right);
127 * @param array $expression Must contain the following keys:
128 * - left: the left-hand side of the expression
129 * - operator: the operator to use for the comparison
130 * - right: the right-hand side of the expression
131 * See the class description for valid values for the left and right
134 * @throws InvalidAssertionException if missing one of the required keys.
135 * @throws InvalidAssertionException if either operand is invalid.
136 * @throws InvalidAssertionException if the operator is not supported.
138 public static function fromArray(array $expression)
140 $required = ['left', 'operator', 'right'];
142 if (count(array_intersect_key($expression, array_flip($required))) < count($required)) {
143 throw new InvalidAssertionException(
144 "Expression assertion requires 'left', 'operator' and 'right' to be supplied"
148 return self
::fromProperties(
150 $expression['operator'],
156 * @param mixed|array $operand
157 * @throws InvalidAssertionException if the operand is invalid.
159 private static function validateOperand($operand)
161 if (is_array($operand) && isset($operand[self
::OPERAND_CONTEXT_PROPERTY
])) {
162 if (! is_string($operand[self
::OPERAND_CONTEXT_PROPERTY
])) {
163 throw new InvalidAssertionException('Expression assertion context operand must be string');
169 * @param string $operand
170 * @throws InvalidAssertionException if the operator is not supported.
172 private static function validateOperator($operator)
174 if (! in_array($operator, self
::$validOperators, true)) {
175 throw new InvalidAssertionException('Provided expression assertion operator is not supported');
182 public function assert(Acl
$acl, RoleInterface
$role = null, ResourceInterface
$resource = null, $privilege = null)
184 return $this->evaluate([
187 'resource' => $resource,
188 'privilege' => $privilege,
193 * @param array $context Contains the acl, privilege, role, and resource
194 * being tested currently.
197 private function evaluate(array $context)
199 $left = $this->getLeftValue($context);
200 $right = $this->getRightValue($context);
202 return static::evaluateExpression($left, $this->operator
, $right);
206 * @param array $context Contains the acl, privilege, role, and resource
207 * being tested currently.
210 private function getLeftValue(array $context)
212 return $this->resolveOperandValue($this->left
, $context);
216 * @param array $context Contains the acl, privilege, role, and resource
217 * being tested currently.
220 private function getRightValue(array $context)
222 return $this->resolveOperandValue($this->right
, $context);
227 * @param array $context Contains the acl, privilege, role, and resource
228 * being tested currently.
230 * @throws RuntimeException if object cannot be resolved in context.
231 * @throws RuntimeException if property cannot be resolved.
233 private function resolveOperandValue($operand, array $context)
235 if (! is_array($operand) ||
! isset($operand[self
::OPERAND_CONTEXT_PROPERTY
])) {
239 $contextProperty = $operand[self
::OPERAND_CONTEXT_PROPERTY
];
241 if (strpos($contextProperty, '.') !== false) { // property path?
242 list($objectName, $objectField) = explode('.', $contextProperty, 2);
243 return $this->getObjectFieldValue($context, $objectName, $objectField);
246 if (! isset($context[$contextProperty])) {
247 throw new RuntimeException(sprintf(
248 "'%s' is not available in the assertion context",
253 return $context[$contextProperty];
257 * @param array $context Contains the acl, privilege, role, and resource
258 * being tested currently.
259 * @param string $objectName Name of object in context to use.
260 * @param string $field
262 * @throws RuntimeException if object cannot be resolved in context.
263 * @throws RuntimeException if property cannot be resolved.
265 private function getObjectFieldValue(array $context, $objectName, $field)
267 if (! isset($context[$objectName])) {
268 throw new RuntimeException(sprintf(
269 "'%s' is not available in the assertion context",
274 $object = $context[$objectName];
275 $accessors = ['get', 'is'];
276 $fieldAccessor = false === strpos($field, '_')
278 : str_replace(' ', '', ucwords(str_replace('_', ' ', $field)));
280 foreach ($accessors as $accessor) {
281 $accessor .= $fieldAccessor;
283 if (method_exists($object, $accessor)) {
284 return $object->$accessor();
288 if (! $this->propertyExists($object, $field)) {
289 throw new RuntimeException(sprintf(
290 "'%s' property cannot be resolved on the '%s' object",
296 return $object->$field;
301 * @param string $right
302 * @param mixed $right
303 * @throws RuntimeException if operand is not supported.
305 private static function evaluateExpression($left, $operator, $right)
308 case self
::OPERATOR_EQ
:
309 return $left == $right;
310 case self
::OPERATOR_NEQ
:
311 return $left != $right;
312 case self
::OPERATOR_LT
:
313 return $left < $right;
314 case self
::OPERATOR_LTE
:
315 return $left <= $right;
316 case self
::OPERATOR_GT
:
317 return $left > $right;
318 case self
::OPERATOR_GTE
:
319 return $left >= $right;
320 case self
::OPERATOR_IN
:
321 return in_array($left, $right);
322 case self
::OPERATOR_NIN
:
323 return ! in_array($left, $right);
324 case self
::OPERATOR_REGEX
:
325 return (bool) preg_match($right, $left);
326 case self
::OPERATOR_NREGEX
:
327 return ! (bool) preg_match($right, $left);
328 case self
::OPERATOR_SAME
:
329 return $left === $right;
330 case self
::OPERATOR_NSAME
:
331 return $left !== $right;
336 * @param object $object
337 * @param string $field
340 private function propertyExists($object, $property)
342 if (! property_exists($object, $property)) {
346 $r = new ReflectionProperty($object, $property);
347 return $r->isPublic();