Symfony Workflow Component and Security Voters? - php

TL;DR: how can you add custom constraints (i.e. security voters) to transitions?
My application needs some workflow management system, so I'd like to try Symfony's new Workflow Component. Let's take a Pull Request workflow as an example.
In this example, only states and their transitions are describes. But what if I want to add other constraints to this workflow? I can image some constraints:
Only admins can accept Pull Request
Users can only reopen their own Pull Request
Users can not reopen PR's older than 1 year
While you can use Events in this case, I don't think that's the best way to handle it, because an event is fired after $workflow->apply(). I want to know beforehand if a user is allowed to change the state, so I can hide or disable the button. (not like this).
The LexikWorkflowBundle solved this problem partially, by adding roles to the steps (transitions). Switching to this bundle might be a good idea, but I'd like to figure out how I can solve this problem without.
What is the best way to add custom entity constraints ('PR older than 1 year can't be reopened') and security constraints ('only admins can accept PR's', maybe by using Symfony's Security Voters) to transitions?
Update:
To clarify: I want to add permission control to my workflow, but that doesn't necessarily mean I want to tightly couple it to the Workflow Component. I'd like to stick to good practices, so the given solution should respect the single responsibility principle.

The best way I found was implementing the AuthorizationChecker in the Workflow's GuardListener.
The demo application gives a good example:
namespace Acme\DemoBundle\Entity\Listener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Workflow\Event\GuardEvent;
class GuardListener implements EventSubscriberInterface
{
public function __construct(AuthorizationCheckerInterface $checker)
{
$this->checker = $checker;
}
public function onTransition(GuardEvent $event)
{
// For all action, user should be logger
if (!$this->checker->isGranted('IS_AUTHENTICATED_FULLY')) {
$event->setBlocked(true);
}
}
public function onTransitionJournalist(GuardEvent $event)
{
if (!$this->checker->isGranted('ROLE_JOURNALIST')) {
$event->setBlocked(true);
}
}
public function onTransitionSpellChecker(GuardEvent $event)
{
if (!$this->checker->isGranted('ROLE_SPELLCHECKER')) {
$event->setBlocked(true);
}
}
public static function getSubscribedEvents()
{
return [
'workflow.article.guard' => 'onTransition',
'workflow.article.guard.journalist_approval' => 'onTransitionJournalist',
'workflow.article.guard.spellchecker_approval' => 'onTransitionSpellChecker',
];
}

Related

Validation or if statements

I’m implementing follow feature and I want to make sure that
A user should not follow the same user twice
Users should not follow themselves
Should I put the aforementioned conditions insife the follow method ?
class User extends model
{
public function follow($user)
{
if($this->is($user) || $this->isFollowing($user))
{
return;
}
}
}
Or should I validate the given user ?
class FollowController
{
public function store()
{
request()->validate([
'username' => ['exists:users,name', new NotYourself, new AlreadyFollowing]
]);
}
}
Or is there a better way ?
B001 is technically correct, but my opinion is that it's not a matter of opinion. Best-practices would dictate going with the second option. Model classes are better suited for context-agnostic handling of tables, not business-logic. And if you apply your rules in the form of input validation, that gives you access to a few other bells and whistles if the validation fails. Specifically, an error response capable of informing the browser what input field to highlight to tell the user they did something wrong. Taken a step further, I'd suggest placing those rules inside a custom FormRequest object instead of the Controller method.
Locking down the endpoint like this is good practice regardless, to prevent low-level hacks with Postman or some-such. But taking a step further back from that, I'd recommend not offering the UI option in the first place. Check out Gates and Policies. You can write compartmentalized access rules, typically intended for authorization, and use them to dictate whether the "follow" button is available at all.

Laravel Event Sourcing (Spatie) - Using projections within business rules

I know that the general concept behind event sourcing is that the state of the application should be able to be replayed from the event stream.
Sometimes, however, we need to get information for business rules from other parts of the system. i.e. An account has a user. A user has a blacklist status which is required to check if they can access/edit the account.
In the below example (purely for demonstration purposes), a user tries to subtract $10 from their account. If a user has been blacklisted, then we do not want to allow them to remove any funds from the account but we do want to record that they have tried to.
After the request is made, we could query the user model to see if the blacklist exists. If true then we can record it and throw the exception.
The user table/model is currently not event-sourced.
Now when we try to replay the event stream to re-build the projections with the state of the user is not being stored in events, it is no longer possible.
So assuming my current example does not work my questions are:
If we were to move the user into an event stored system (in a different aggregate but all events within the same event-stream) then would it be acceptable to use read models within business rules?
Is there any way we can mix event-sourced and CRUD into the same system when they may depend on each other for business rules.
public function subtractMoney(int $amount)
{
if ($this->accountOwnerIsBlacklisted()){
$this->recordThat(new UserActionBlocked());
throw CouldNotSubtractMoney::ownerBlocked();
}
if (!$this->hasSufficientFundsToSubtractAmount($amount)) {
$this->recordThat(new AccountLimitHit());
if ($this->needsMoreMoney()) {
$this->recordThat(new MoreMoneyNeeded());
}
$this->persist();
throw CouldNotSubtractMoney::notEnoughFunds($amount);
}
$this->recordThat(new MoneySubtracted($amount));
}
private function accountOwnerIsBlacklisted(): bool
{
return $this->accountRepositry()->ownerUser()->isBlackListed();
}
Since you are basically working with DDD (without mentioning it) the answer could lie in the definitions there. In DDD you are supposed to define the boundaries of each aggregate root. Each aggregate root should not store any dependencies to other aggregate roots (The Spatie package doesn't even support it). It should only be made up of the events, which then become the single source of truth.
Given your example, it seems that the blocking of a user is not due to negative events on his account, but rather due to something that happened in relation to his user (account owner). The keyword here seems to be "owner". If you want to store the fact that the user action of trying to withdraw money happened, then you could still apply the event, but the reason would, in this case, come from another aggregate "the user". It doesn't matter if the user itself is event sourced, but the user entity has the method to check if the user is blocked, and therefore it is the business rule in your system that he is not allowed to make withdrawals from the account.
If you cannot model these two together, then I would suggest that you design a domain service which can handle this command. Try to keep them as a part of your model to avoid making your domain model anaemic, if you can.
<?php
class AccountWithdrawalService
{
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function withdraw($userId, $accountId, $amount)
{
$user = $this->userRepository->find($userId);
// You might inject AccountAggregateRoot too.
$account = AccountAggregateRoot::retrieve($accountId);
if(!$user->isBlackListed())
{
$account->subtractMoney($amount);
}
else
{
// Here we record the unhappy road :-(
$account->moneySubtractionBlocked($amount);
}
$account->persist();
}
}
PS: A further possibility is to inject your userRepository in the actual method handling the withdrawal, as long as the userRepository is not a full dependency of the AccountAggregateRoot. This, I believe, is highly discussed.
Give the Account a UserBlacklistedEvent and use this for validation in the Account. Don't record failed attempts unless thats a reporting requirement.
<?php
class Account {
private bool blacklisted;
function blacklist() {
$this->recordThat(new Blacklisted());
}
public function subtractMoney(int $amount)
{
if ($this->blacklisted) {
throw new DomainException("Why on earth has this backend developer let fella make queries to their account. fk em - wait call security we being attaced");
}
...
<?php
class BlacklistService
{
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function blacklist($userId, $accountId)
{
$user = $this->userRepository->find($userId);
// You might inject AccountAggregateRoot too.
$account = AccountAggregateRoot::retrieve($user->accountUuid);
$user->blacklist();
$user->save();
$account->blacklisted(); // no need for userid, in most cases
$account->persist();
}
}
<?php
class AccountCreateService
{
function create() {
$user = $this->userRepository->find($userId);
if ($user->blacklisted) {
throw new DomainException("Why on earth has the frontend developer called this.");
}
$account = AccountAggregateRoot::retrieve($user->accountUuid);
...
Even better: don't throw a Exception in Account AR. just return void or and $this->recordThat(new SubtractionEventOnBlacklistedEvent). Since its invariant is protected. Why throw an event? or if its invarient is protected then the invalid case ins't an exceptional circumstance
The exception after all won't do anything for the user. Its unlightly you wan't them reading the exception screen. And then in this case, you could setup some alternate logging perhaps

Symfony 4 : Logout a users in a custom controller

I have followed the tutorial to make may application able to logout users simply by calling a route like /logout (Via the Security module as described in the official documentation). It works.
Now I would like to logout the user (still logged via the described in the doc "Remember me" function) in my own controllers (For example before an email validation, in case another session is still opened under another account).
But none of my methods works, it makes me crazy. I have tried $session->clear(), $session->invalidate(), $request->getSession->clear(), $request->getSession->Invalidate(), etc. etc. Nothing works.
So my question are, please: How do you do it? How should I handle this case? Is it related to the "remember me" functionality (maybe it's managed in another cookie or something?) ?
Thanks in advance
Your guess might be right, that the issue could be related to the remember me functionality as this will use cookies to store the token, instead of the session, and therefore need a different LogoutHandler.
Symfony provides multiple ways to handle authentication and you will need the correct LogoutHandler(s) depending on your current settings.
Solving your issue is surprisingly hard if you don't just want to redirect the user to the logout path. The "best" way I can think of right now, is simulating a logout-request by building the Request-object manually and then dispatching a GetResponseEvent with it so, that the LogoutListener will be triggered. Dispatching the event might have weird side effects, so you might even want to trigger the listener directly. It could look something like this:
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class MyController
{
private $kernel;
private $logoutListener;
public function __construct(HttpKernelInterface $kernel, LogoutListenerInterface $logoutListener)
{
$this->kernel = $kernel;
$this->logoutListener = $logoutListener;
}
private function customLogout()
{
// This needs to be updated whenever the logout path in your security.yaml changes. Probably something you want to improve on.
$request = Request::create('/logout');
$event = new GetResponseEvent($this->kernel, $request);
$this->logoutListener->handle($event);
}
public function someAction()
{
$this->customLogout();
// Do whatever you want to do for your request
}
}
I don't think this is a good solution as interfering with the security system is inherently dangerous. Directly calling the LogoutListener and passing around the kernel are also a bit iffy. To be honest I'm not even 100% sure this will work, but this is the closest to what I could come up with as a possible solution to your problem. You might want to rethink what you are doing and find an alternative approach.

Controller as Service - How to pass and return values in an advanced case?

Using Symfony, I am displaying a table with some entries the user is able to select from. There is a little more complexity as this might include calling some further actions e. g. for filtering the table entries, sorting by different criteria, etc.
I have implemented the whole thing in an own bundle, let's say ChoiceTableBundle (with ChoiceTableController). Now I would like to be able to use this bundle from other bundles, sometimes with some more parametrization.
My desired workflow would then look like this:
User is currently working with Bundle OtherBundle and triggers chooseAction.
chooseAction forwards to ChoiceTableController (resp. its default entry action).
Within ChoiceTableBundle, the user is able to navigate, filter, sort, ... using the actions and routing supplied by this bundle.
When the user has made his choice, he triggers another action (like choiceFinishedAction) and the control flow returns to OtherBundle, handing over the results of the users choice.
Based on these results, OtherBundle can then continue working.
Additionally, OtherOtherBundle (and some more...) should also be able to use this workflow, possibly passing some configuration values to ChoiceTableBundle to make it behave a little different.
I have read about the "Controller as Service" pattern of Symfony 2 and IMHO it's the right approach here (if not, please tell me ;)). So I would make a service out of ChoiceTableController and use it from the other bundles. Anyway, with the workflow above in mind, I don't see a "good" way to achieve this:
How can I pass over configuration parameters to ChoiceTableBundle (resp. ChoiceTableController), if neccessary?
How can ChoiceTableBundle know from where it was called?
How can I return the results to this calling bundle?
Basic approaches could be to store the values in the session or to create an intermediate object being passed. Both do not seem particularly elegant to me. Can you please give me a shove in the right direction? Many thanks in advance!
The main question is if you really need to call your filtering / searching logic as a controller action. Do you really need to make a request?
I would say it could be also doable just by passing all the required data to a service you define.
This service you should create from the guts of your ChoiceTableBundleand let both you ChoiceTableBundle and your OtherBundle to use the extracted service.
service / library way
// register it in your service container
class FilteredDataProvider
{
/**
* #return customObjectInterface or scallar or whatever you like
*/
public function doFiltering($searchString, $order)
{
return $this->filterAndReturnData($searchString, $order)
}
}
...
class OtherBundleController extends Controller {
public function showStuffAction() {
$result = $this->container->get('filter_data_provider')
->doFiltering('text', 'ascending')
}
}
controller way
The whole thing can be accomplished with the same approach as lipp/imagine bundle uses.
Have a controller as service and call/send all the required information to that controller when you need some results, you can also send whole request.
class MyController extends Controller
{
public function indexAction()
{
// RedirectResponse object
$responeFromYourSearchFilterAction = $this->container
->get('my_search_filter_controller')
->filterSearchAction(
$this->request, // http request
'parameter1' // like search string
'parameterX' // like sorting direction
);
// do something with the response
// ..
}
}
A separate service class would be much more flexible. Also if you need other parameters or Request object you can always provide it.
Info how to declare controller as service is here:
http://symfony.com/doc/current/cookbook/controller/service.html
How liip uses it:
https://github.com/liip/LiipImagineBundle#using-the-controller-as-a-service

Multiple Instances (2) of Zend_Auth

I have a CMS built on the Zend Framework. It uses Zend_Auth for "CMS User" authentication. CMS users have roles and permissions that are enforced with Zend_Acl. I am now trying to create "Site Users" for things like an online store. For simplicity sake I would like to use a separate instance of Zend_Auth for site users. Zend_Auth is written as a singleton, so I'm not sure how to accomplish this.
Reasons I don't want to accomplish this by roles:
Pollution of the CMS Users with Site Users (visitors)
A Site User could accidentally get elevated permissions
The users are more accurately defined as different types than different roles
The two user types are stored in separate databases/tables
One user of each type could be signed in simultaneously
Different types of information are needed for the two user types
Refactoring that would need to take place on existing code
In that case, you want to create your own 'Auth' class to extend and remove the 'singleton' design pattern that exists in Zend_Auth
This is by no means complete, but you can create an instance and pass it a 'namespace'. The rest of Zend_Auth's public methods should be fine for you.
<?php
class My_Auth extends Zend_Auth
{
public function __construct($namespace) {
$this->setStorage(new Zend_Auth_Storage_Session($namespace));
// do other stuff
}
static function getInstance() {
throw new Zend_Auth_Exception('I do not support getInstance');
}
}
Then where you want to use it, $auth = new My_Auth('CMSUser'); or $auth = new My_Auth('SiteUser');
class App_Auth
{
const DEFAULT_NS = 'default';
protected static $instance = array();
protected function __clone(){}
protected function __construct() {}
static function getInstance($namespace = self::DEFAULT_NS) {
if(!isset(self::$instance[$namespace]) || is_null(self::$instance[$namespace])) {
self::$instance[$namespace] = Zend_Auth::getInstance();
self::$instance[$namespace]->setStorage(new Zend_Auth_Storage_Session($namespace));
}
return self::$instance[$namespace];
}
}
Try this one , just will need to use App_Auth instead of Zend_Auth everywhere, or App_auth on admin's area, Zend_Auth on front
that is my suggestion :
i think you are in case that you should calculate ACL , recourses , roles dynamically ,
example {md5(siteuser or cmsuser + module + controller)= random number for each roles }
and a simple plugin would this role is allowed to this recourse
or you can build like unix permission style but i guess this idea need alot of testing
one day i will build one like it in ZF :)
i hope my idea helps you
You're mixing problems. (not that I didn't when I first faced id)
Zend_Auth answers the question "is that user who he claims to be"? What you can do is to add some more info to your persistence object. Easiest option is to add one more column into your DB and add it to result.

Categories