Skip to content

Commit

Permalink
FEATURE (WIP): Draft of adjusted security framework for Nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
skurfuerst committed Mar 12, 2020
1 parent ece5861 commit 2ce2b4f
Show file tree
Hide file tree
Showing 17 changed files with 1,236 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php
namespace Neos\EventSourcedContentRepository\Security\Authorization\Privilege\Node;

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\Eel\CompilingEvaluator;
use Neos\Eel\Context;
use Neos\Flow\Aop\Pointcut\PointcutFilterInterface;
use Neos\Flow\Security\Authorization\Privilege\AbstractPrivilege;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilege;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilegeInterface;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilegeSubject;
use Neos\Flow\Security\Authorization\Privilege\PrivilegeSubjectInterface;
use Neos\Flow\Security\Authorization\Privilege\PrivilegeTarget;
use Neos\Flow\Security\Exception\InvalidPrivilegeTypeException;

/**
* An abstract node privilege acting as a base class for other
* node privileges restricting operations and data on nodes.
*/
abstract class AbstractNodePrivilege extends AbstractPrivilege implements MethodPrivilegeInterface
{
/**
* @var CompilingEvaluator
*/
protected $eelCompilingEvaluator;

/**
* @var string
*/
protected $nodeContextClassName = NodePrivilegeContext::class;

/**
* @var NodePrivilegeContext
*/
protected $nodeContext;

/**
* @var MethodPrivilegeInterface
*/
protected $methodPrivilege;

/**
* @var boolean
*/
protected $initialized = false;

/**
* Constructor
*
* @param PrivilegeTarget $privilegeTarget
* @param string $matcher
* @param string $permission
* @param $parameters
*/
public function __construct(PrivilegeTarget $privilegeTarget, string $matcher, string $permission, $parameters)
{
parent::__construct($privilegeTarget, $matcher, $permission, $parameters);
$this->cacheEntryIdentifier = null;
}

/**
* @return void
*/
public function initialize()
{
if ($this->initialized) {
return;
}
$this->initialized = true;
$this->eelCompilingEvaluator = $this->objectManager->get(CompilingEvaluator::class);
$this->nodeContext = new $this->nodeContextClassName();
$this->initializeMethodPrivilege();
}

/**
* @return void
*/
protected function buildCacheEntryIdentifier()
{
$this->cacheEntryIdentifier = md5($this->privilegeTarget->getIdentifier() . '__methodPrivilege' . '|' . $this->buildMethodPrivilegeMatcher());
}

/**
* Unique identifier of this privilege
*
* @return string
*/
public function getCacheEntryIdentifier(): string
{
if ($this->cacheEntryIdentifier === null) {
$this->buildCacheEntryIdentifier();
}

return $this->cacheEntryIdentifier;
}

/**
* @param PrivilegeSubjectInterface|NodePrivilegeSubject|MethodPrivilegeSubject $subject (one of NodePrivilegeSubject or MethodPrivilegeSubject)
* @return boolean
* @throws InvalidPrivilegeTypeException
*/
public function matchesSubject(PrivilegeSubjectInterface $subject)
{
if ($subject instanceof NodePrivilegeSubject === false && $subject instanceof MethodPrivilegeSubject === false) {
throw new InvalidPrivilegeTypeException(sprintf('Privileges of type "%s" only support subjects of type "%s" or "%s", but we got a subject of type: "%s".', AbstractNodePrivilege::class, NodePrivilegeSubject::class, MethodPrivilegeSubject::class, get_class($subject)), 1417014368);
}

if ($subject instanceof MethodPrivilegeSubject) {
$this->initializeMethodPrivilege();
return $this->methodPrivilege->matchesSubject($subject);
}

$this->initialize();
$nodeContext = new $this->nodeContextClassName($subject->getNode());
$eelContext = new Context($nodeContext);

return $this->eelCompilingEvaluator->evaluate($this->getParsedMatcher(), $eelContext);
}

/**
* @param string $className
* @param string $methodName
* @return boolean
*/
public function matchesMethod($className, $methodName)
{
$this->initializeMethodPrivilege();
return $this->methodPrivilege->matchesMethod($className, $methodName);
}

/**
* @return PointcutFilterInterface
*/
public function getPointcutFilterComposite()
{
$this->initializeMethodPrivilege();
return $this->methodPrivilege->getPointcutFilterComposite();
}

/**
* @throws \Neos\Flow\Security\Exception
*/
protected function initializeMethodPrivilege()
{
if ($this->methodPrivilege !== null) {
return;
}
$methodPrivilegeMatcher = $this->buildMethodPrivilegeMatcher();
$methodPrivilegeTarget = new PrivilegeTarget($this->privilegeTarget->getIdentifier() . '__methodPrivilege', MethodPrivilege::class, $methodPrivilegeMatcher);
$methodPrivilegeTarget->injectObjectManager($this->objectManager);
$this->methodPrivilege = $methodPrivilegeTarget->createPrivilege($this->getPermission(), $this->getParameters());
}

/**
* Evaluates the matcher with this objects nodeContext and returns the result.
*
* @return mixed
*/
protected function evaluateNodeContext()
{
$eelContext = new Context($this->nodeContext);
return $this->eelCompilingEvaluator->evaluate($this->getParsedMatcher(), $eelContext);
}

/**
* @return string
*/
abstract protected function buildMethodPrivilegeMatcher();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
namespace Neos\EventSourcedContentRepository\Security\Authorization\Privilege\Node;

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\ContentRepository\Domain\Projection\Content\TraversableNodeInterface;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilegeSubject;
use Neos\Flow\Security\Authorization\Privilege\PrivilegeSubjectInterface;
use Neos\Flow\Security\Exception\InvalidPrivilegeTypeException;

/**
* Base class for privileges restricting node properties.
*/
abstract class AbstractNodePropertyPrivilege extends AbstractNodePrivilege
{
/**
* @var PropertyAwareNodePrivilegeContext
*/
protected $nodeContext;

/**
* @var string
*/
protected $nodeContextClassName = PropertyAwareNodePrivilegeContext::class;

/**
* With this mapping we can treat methods like properties. E.g. we want to be able to have a property "hidden" even though there is no real property
* called like this. Instead the set/getHidden() methods should match this "property".
*
* @var array
*/
protected $methodNameToPropertyMapping = [];

/**
* @param PrivilegeSubjectInterface|PropertyAwareNodePrivilegeSubject|MethodPrivilegeSubject $subject
* @return boolean
* @throws InvalidPrivilegeTypeException
*/
public function matchesSubject(PrivilegeSubjectInterface $subject)
{
if ($subject instanceof PropertyAwareNodePrivilegeSubject === false && $subject instanceof MethodPrivilegeSubject === false) {
throw new InvalidPrivilegeTypeException(sprintf('Privileges of type "%s" only support subjects of type "%s" or "%s", but we got a subject of type: "%s".', ReadNodePropertyPrivilege::class, PropertyAwareNodePrivilegeSubject::class, MethodPrivilegeSubject::class, get_class($subject)), 1417018448);
}

$this->initialize();
$this->evaluateNodeContext();
if ($subject instanceof MethodPrivilegeSubject) {
if ($this->methodPrivilege->matchesSubject($subject) === false) {
return false;
}

$joinPoint = $subject->getJoinPoint();

// if the context isn't restricted to certain properties, it matches *all* properties
if ($this->nodeContext->hasProperties()) {
$methodName = $joinPoint->getMethodName();
$actualPropertyName = null;

if (isset($this->methodNameToPropertyMapping[$methodName])) {
$propertyName = $this->methodNameToPropertyMapping[$methodName];
} else {
$propertyName = $joinPoint->getMethodArgument('propertyName');
}
if (!in_array($propertyName, $this->nodeContext->getNodePropertyNames())) {
return false;
}
}

/** @var TraversableNodeInterface $node */
$node = $joinPoint->getProxy();
$nodePrivilegeSubject = new NodePrivilegeSubject($node);
return parent::matchesSubject($nodePrivilegeSubject);
}
if ($subject->hasPropertyName() && in_array($subject->getPropertyName(), $this->nodeContext->getNodePropertyNames()) === false) {
return false;
}
return parent::matchesSubject($subject);
}

/**
* @return array
*/
public function getNodePropertyNames()
{
return $this->nodeContext->getNodePropertyNames();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php
namespace Neos\EventSourcedContentRepository\Security\Authorization\Privilege\Node;

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

use Neos\ContentRepository\Domain\Projection\Content\TraversableNodeInterface;
use Neos\EventSourcedContentRepository\Domain\Context\NodeAggregate\Command\CreateNodeAggregateWithNode;
use Neos\EventSourcedContentRepository\Domain\Context\NodeAggregate\Command\CreateNodeVariant;
use Neos\Flow\Security\Authorization\Privilege\Method\MethodPrivilegeSubject;
use Neos\Flow\Security\Authorization\Privilege\PrivilegeSubjectInterface;
use Neos\Flow\Security\Exception\InvalidPrivilegeTypeException;

/**
* A privilege to restrict node creation
*/
class CreateNodePrivilege extends AbstractNodePrivilege
{
/**
* @var CreateNodePrivilegeContext
*/
protected $nodeContext;

/**
* @var string
*/
protected $nodeContextClassName = CreateNodePrivilegeContext::class;

/**
* @param PrivilegeSubjectInterface|CreateNodePrivilegeSubject|MethodPrivilegeSubject $subject
* @return boolean
* @throws InvalidPrivilegeTypeException
*/
public function matchesSubject(PrivilegeSubjectInterface $subject)
{
if ($subject instanceof CreateNodePrivilegeSubject === false && $subject instanceof MethodPrivilegeSubject === false) {
throw new InvalidPrivilegeTypeException(sprintf('Privileges of type "%s" only support subjects of type "%s" or "%s", but we got a subject of type: "%s".', CreateNodePrivilege::class, CreateNodePrivilegeSubject::class, MethodPrivilegeSubject::class, get_class($subject)), 1417014353);
}

$this->initialize();
$this->evaluateNodeContext();
if ($subject instanceof MethodPrivilegeSubject) {
if ($this->methodPrivilege->matchesSubject($subject) === false) {
return false;
}

$joinPoint = $subject->getJoinPoint();
$allowedCreationNodeTypes = $this->nodeContext->getCreationNodeTypes();
$actualNodeType = $joinPoint->getMethodName() === 'createNodeFromTemplate' ? $joinPoint->getMethodArgument('nodeTemplate')->getNodeType()->getName() : $joinPoint->getMethodArgument('nodeType')->getName();

if ($allowedCreationNodeTypes !== [] && !in_array($actualNodeType, $allowedCreationNodeTypes)) {
return false;
}

/** @var TraversableNodeInterface $node */
$node = $joinPoint->getProxy();
$nodePrivilegeSubject = new NodePrivilegeSubject($node);
$result = parent::matchesSubject($nodePrivilegeSubject);
return $result;
}

if ($this->nodeContext->getCreationNodeTypes() === [] || ($subject->hasCreationNodeType() === false) || in_array($subject->getCreationNodeType()->getName(), $this->nodeContext->getCreationNodeTypes()) === true) {
return parent::matchesSubject($subject);
}
return false;
}

/**
* @return array $creationNodeTypes
*/
public function getCreationNodeTypes()
{
return $this->nodeContext->getCreationNodeTypes();
}

/**
* @return string
*/
protected function buildMethodPrivilegeMatcher()
{
return 'method(' . CreateNodeVariant::class . '->__construct()) && method(' . CreateNodeAggregateWithNode::class . '->__construct())';
}
}
Loading

0 comments on commit 2ce2b4f

Please sign in to comment.