-
Notifications
You must be signed in to change notification settings - Fork 111
Added ability to set multiple assertions and their condition for permissions #320
base: master
Are you sure you want to change the base?
Conversation
I like the idea. What about bringing a new "AssertionSet" class that would contain the two constants, and abstract the logic of checking all the assertions and returning a boolean, so the |
That actually sounds good. I'll work on it within next few days. |
This is a very good idea! It would help us keep things better organized. |
@DavidHavl I like this idea! Now work on V3 is progressing it would be bring this feature there too. |
Thanks @basz, I have been extremely busy with work past few months but I should have more time now for this. |
Is there still progress on this? |
… permissions to use new AssertionSet.
1 similar comment
1 similar comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks very solid.
Perhaps the way an AssertionSet is created could be automated. Look at 'RoutePermissionsGuard' configuration for inspiration if you're up to it. cc @prolic
@@ -122,19 +122,15 @@ public function getIdentity() | |||
public function isGranted($permission, $context = null) | |||
{ | |||
$roles = $this->roleService->getIdentityRoles(); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need to remove these empty lines
Also coverage decreased, can you add some more tests, please? |
@basz that was my initial idea behind this but it was not accepted. |
Ok, remember why? Perhaps was it for good reason that I am unfamiliar with? |
1 similar comment
1 similar comment
1 similar comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tested locally just on github, but seems good feature just dont't like that it can have big performance change. Also I guess not everything tested, if I found a bug and tests did not.
|
||
// move assertion definition under a key 'assertions'. | ||
if (!isset($assertion['assertions'])) { | ||
$assertion['assertions'] = (array)$assertion; | ||
} else if (!is_array($assertion['assertions'])) { | ||
} elseif (!is_array($assertion['assertions'])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove else, should be:
if (!is_array($assertion['assertions'])) {
* @param mixed $context | ||
* @return bool | ||
*/ | ||
public function assert(AuthorizationService $authorizationService, $context = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe not logical but it's possible to have zero assertions here then maybe return true by default or throw exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An empty assertion is not logical indeed and should either return FALSE or an exception.
Better to have defensive defaults.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Thanx.
// move assertion definition under a key 'assertions'. | ||
if (!isset($assertion['assertions'])) { | ||
$assertion['assertions'] = (array)$assertion; | ||
} elseif (!is_array($assertion['assertions'])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove else, this should be:
if (!is_array($assertion['assertions'])) {
* @licence MIT | ||
*/ | ||
|
||
class AssertionSet implements AssertionInterface, \IteratorAggregate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think most of the setters/getters are not required and/or can be private or removed only assert() method is required by interface so why expose those while we can build AssertionSet thought constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are not required but I think it may be a good idea to have them, in case people want to use the class their way (see docs).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course not after it is added to AuthorizationService, but when specifying assertion map. See my other comment bellow.
* | ||
* @return bool true if the assertion is defined, false otherwise | ||
*/ | ||
public function hasAssertion($name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure but do we use this somewhere outside this class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes people can when they use the class directly (see docs).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okey maybe they could but still not convinced. Even if they want to modify it what is bad I think they could create new one instead. because reusing same set on different permissions and randomly modify them at the same time can lead to big problems. And if users would want those evil methods they could extend the class anyway no? so why add those we don't want to maintain +6 methods for just in case users would need them.
but again @basz @prolic @bakura10 @danizord what you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure you understand. This is to be used for example when creating assertion map. I was imagining usage of it for example like this:
Creating a factory where one can do anything they want
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use ZfcRbac\Assertion\AssertionSet;
class MyAssertionSetFactory implements FactoryInterface
{
/**
* {@inheritDoc}
*
* @return AssertionSet
*/
public function __invoke(ContainerInterface $container, $name, array $options = null)
{
$assertionManager = $container->get('ZfcRbac\Assertion\AssertionPluginManager');
$assertion1 = $assertionManager->get('myAssertion1');
$assertion2 = $assertionManager->get('myAssertion2');
// create instance, set condition and add assertions
$assertionSet = new AssertionSet();
$assertionSet->setCondition(AssertionSet::CONDITION_OR);
$assertionSet->setAssertions([$assertion1, $assertion2]);
return $assertionSet;
}
/**
* {@inheritDoc}
*
* For use with zend-servicemanager v2; proxies to __invoke().
*
* @param ServiceLocatorInterface $container
* @return \ZfcRbac\Assertion\AssertionSet
*/
public function createService(ServiceLocatorInterface $container)
{
// Retrieve the parent container when under zend-servicemanager v2
if (method_exists($container, 'getServiceLocator')) {
$container = $container->getServiceLocator() ?: $container;
}
return $this($container, AssertionSet::class);
}
}
And then add it to assertion manager and assertion map config:
return [
'zfc_rbac' => [
'assertion_manager' => [
'factories' => [
'myAssertionSet' => MyAssertionSetFactory::class
]
],
'assertion_map' => [
'myPermission' => 'myAssertion', // single assertion
'myPermission2' => 'myAssertionSet' // multiple assertions in set
]
]
];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could do the same without those methods:
$assertionManager = $container->get('ZfcRbac\Assertion\AssertionPluginManager');
$assertion1 = $assertionManager->get('myAssertion1');
$assertion2 = $assertionManager->get('myAssertion2');
// create instance, set condition and add assertions
$assertionSet = new AssertionSet([
'assertions' => [$assertion1, $assertion2],
'condition' => AssertionSet::CONDITION_OR
]);
and this would prevent from changing later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for immutable approach. That makes unit testing way easier and confident since you don't need to test a lot of possible different states.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. That is a good point.
Will rewrite.
Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I said but will repeat: not good that we can't do this:
$assertionSet = new AssertionSet([
'assertions' => ['myAssertion1', 'myAssertion2'],
'condition' => AssertionSet::CONDITION_OR
]);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@svycka great, I am working on it.
// retrieve an actual instance from assertion plugin manager if necessary | ||
foreach ($assertion['assertions'] as $key => $value) { | ||
if (is_string($value)) { | ||
$assertion['assertions'][$key] = $this->assertionPluginManager->get($value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetching assertions would be better when they are really needed in assert()
method in this case AssertionSet::assert()
@@ -88,7 +112,10 @@ public function setAssertion($permission, $assertion) | |||
*/ | |||
public function setAssertions(array $assertions) | |||
{ | |||
$this->assertions = $assertions; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't know if this is good creating assertions while setting them. Maybe we could create AssertionSet only if we need in assert()
method?
} elseif (is_string($assertion)) { | ||
$assertion = $this->assertionPluginManager->get($assertion); | ||
|
||
return $assertion->assert($this, $context); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe add this:
elseif (is_array($assertion)) {
$assertionSet = new AssertionSet($assertion, $this->assertionPluginManager);
return $assertionSet->assert($this, $context);
}
see my above comments why
*/ | ||
public function assert(AuthorizationService $authorizationService, $context = null) | ||
{ | ||
if (self::CONDITION_AND === $this->condition) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this class not a final then maybe AssertionSet::CONDITION_AND
return true; | ||
} | ||
|
||
if (self::CONDITION_OR === $this->condition) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this class not a final then maybe AssertionSet::CONDITION_OR
/** | ||
* @var $condition string | ||
*/ | ||
protected $condition = 'AND'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AssertionSet::CONDITION_AND
instead of AND
1 similar comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just left some suggestions
return false; | ||
} | ||
|
||
throw new InvalidArgumentException(sprintf( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably be checked at construction time
* Retrieve an external iterator | ||
* @return \ArrayIterator | ||
*/ | ||
public function getIterator() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's no reason to expose this method and other getters?
// if is name of the assertion, retrieve an actual instance from assertion plugin manager | ||
if (is_string($assertion)) { | ||
$assertion = $this->assertionPluginManager->get($assertion); | ||
} elseif (is_array($assertion)) { // else if multiple assertion definition, create assertion set. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the following approach?
assertion_map
config key accepts onlystring => string
(permission name => assertion name)- Another config key for
assertion_sets
that acceptsstring => array
(assertion name => assertion config) - A default
AssertionSetFactory
implementation that reads theassertion_sets
config key and buildsAssertionSet
AuthorizationService
always fetch assertions fromAssertionPluginManager
… assert method of AuthorizationService.
1 similar comment
@danizord the approach you describe is interesting, but I have a concern from usability point of view. I mean, I think it may be more confusing and less intuitive for people wanting to use the module to have yet another separate config for sets. Plus I am not sure about performance implications either. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recoded with most of the suggestions and changes implemented. Please review and let me know if there is anything else that is an issue.
Thank you.
* @param mixed $context | ||
* @return bool | ||
*/ | ||
public function assert(AuthorizationService $authorizationService, $context = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@@ -150,10 +177,6 @@ protected function assert($assertion, $context = null) | |||
return $assertion($this, $context); | |||
} elseif ($assertion instanceof AssertionInterface) { | |||
return $assertion->assert($this, $context); | |||
} elseif (is_string($assertion)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now back, you are right, it makes it more performant to have the retrieval only when it's called/needed.
First yes you moved I don't like that And I still would like to change protected function assert($assertion, $context = null)
{
$assertion = new AssertionSet($assertion, $this->assertionPluginManager);
return $assertion->assert($this, $context);
} this is also return [
'zfc_rbac' => [
'assertion_map' => [
// single assertion
'myPermission' => 'myAssertion',
// assertion set with default condition `and`
'myPermission2' => [
'myAssertion',
'myAssertion2',
]
]
]
]; or if you don't like adding public function getAssertion(string $assertion)
{
return $this->assertionPluginManager->get($assertion);
} |
@svycka I see your point in case of |
It is not just or condition with and this also valid if first fail all On Oct 31, 2016 02:32, "David Havl" [email protected] wrote:
|
Hey guys, I don't think I will have enough time to do more changes on this in next few months (got an urgent project that doesn't use this), so feel free to do a pull |
* @author David Havl | ||
* @licence MIT | ||
*/ | ||
class AssertionSet implements AssertionInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's mark final
the author expressed he didn't have time to continue it. We now have this functionality in the develop branch and #379 could be backported to master if anyone needs it. (hence the label change) |
Great, thanks for finishing it up @basz ! I indeed was not able to work on it as much as I would like to any more and it was good enough for the project I was working on at that time. |
An update to V2 to allow set multiple assertions as well as their condition for permissions.