Link to secured Action in Flow 3.0.8

Hello my Co-coder friends! :slight_smile:

I have a Policy.yaml like the simplified one below:

privilegeTargets:
  TYPO3\Flow\Security\Authorization\Privilege\Method\MethodPrivilege:
    publicAdminActions:
      matcher: method(My\Package\Controller\AdminController->indexAction())
    AdminActions:
      matcher: method(My\Package\Controller\AdminController->(edit|update)Action())
roles:
  'My.Package:User':
    privileges:
      -
        privilegeTarget: publicAdminActions
        permission: GRANT
  'My.Package:Admin':
    parentRoles:
      - 'My.Package:User'
    privileges:
      -
        privilegeTarget: AdminActions
        permission: GRANT

Now i’m looking for a behavior like described below - but dont know how to do it or what i’m missing.

In my Templates i want to place a link like this: <f:link.action action="edit" arguments="[...]">[Link text here]</f:link.action>

  • If a User with Role “My.Package:User” sees the template, i want him just to see the normal unlinked linktext.
  • But if a User with Role “My.Package:Admin” sees the template, i want him to see the normal full link to edit action.

Is that allready possible in any way? Or do i have to clone the link-viewHelper and implement it myself?

Greetings,
Jan

The simple solution would be to have an if condition:

<f:security.ifHasRole role="My.Package:Admin">
  <f:then>
    <f:link.action action="edit" arguments="[...]">[Link text here]</f:link.action>
  </f:then>
  <f:else>
    Unlinked link text
  </f:else>
</f:security.ifHasRole>

I don’t know of a ViewHelper capable of what you want, but I guess as you suggested you could create an own VH.

Hey,

I’m using your suggestion allready for parts of my navigation structure - but it doen’t seem to be quite useable for “normal” links. So i took the link.action and ifHasRole viewhelpers and made a frankenstein-hybrid-like viewhelper :slight_smile: :wink:

I ended up with this:

<?php
namespace MY\Package\ViewHelpers\Link;

/*
 * Copyright here
 */

use TYPO3\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
use TYPO3\Fluid\Core\ViewHelper;
use TYPO3\Flow\Annotations as Flow;
use TYPO3\Flow\Security\Account;
use TYPO3\Flow\Security\Context;
use TYPO3\Flow\Security\Policy\PolicyService;
use TYPO3\Flow\Security\Policy\Role;
use TYPO3\Fluid\Core\ViewHelper\AbstractConditionViewHelper;

/**
 * A view helper for creating links to actions - if allowed to!

 * = Example =

 * <code title="With Security enabled">
 * <my:link.action action="edit" arguments="{account: user}" role="MY.Package:Admin">Edit the User!</my:link.action>
 * </code>
 * <output>
 * (If actual authenticated User has the Role MY.Package:Admin)
 * <a href="/Package/Controller/edit?account%5B__identity%5D=0123456a-c1d2-1234-1234-1f234567890d">Edit the User!</a>
 * (If not just plain text is shown)
 * Edit the User!
 * </output>

 *
 * @api
 */
class ActionViewHelper extends AbstractTagBasedViewHelper
{
    /**
     * @var string
     */
    protected $tagName = 'a';

    /**
     * @Flow\Inject
     * @var Context
     */
    protected $securityContext;

    /**
     * @Flow\Inject
     * @var PolicyService
     */
    protected $policyService;

    /**
     * Initialize arguments
     *
     * @return void
     * @api
     */
    public function initializeArguments()
    {
        $this->registerUniversalTagAttributes();
        $this->registerTagAttribute('name', 'string', 'Specifies the name of an anchor');
        $this->registerTagAttribute('rel', 'string', 'Specifies the relationship between the current document and the linked document');
        $this->registerTagAttribute('rev', 'string', 'Specifies the relationship between the linked document and the current document');
        $this->registerTagAttribute('target', 'string', 'Specifies where to open the linked document');
    }

    /**
     * Render the link.
     *
     * @param string $action Target action
     * @param array $arguments Arguments
     * @param string $controller Target controller. If NULL current controllerName is used
     * @param string $package Target package. if NULL current package is used
     * @param string $subpackage Target subpackage. if NULL current subpackage is used
     * @param string $section The anchor to be added to the URI
     * @param string $format The requested format, e.g. ".html"
     * @param array $additionalParams additional query parameters that won't be prefixed like $arguments (overrule $arguments)
     * @param boolean $addQueryString If set, the current query parameters will be kept in the URI
     * @param array $argumentsToBeExcludedFromQueryString arguments to be removed from the URI. Only active if $addQueryString = TRUE
     * @param boolean $useParentRequest If set, the parent Request will be used instead of the current one
     * @param boolean $absolute By default this ViewHelper renders links with absolute URIs. If this is FALSE, a relative URI is created instead
     *
     * @param string $role The role or role identifier
     * @param string $rolePackageKey PackageKey of the package defining the role
     * @param Account $account If specified, this subject of this check is the given Account instead of the currently authenticated account
     *
     * @return string The rendered link
     * @throws ViewHelper\Exception
     * @api
     */
    public function render($action, $arguments = array(), $controller = null, $package = null, $subpackage = null, $section = '', $format = '', array $additionalParams = array(), $addQueryString = false, array $argumentsToBeExcludedFromQueryString = array(), $useParentRequest = false, $absolute = true,
    $role = null, $rolePackageKey = null, Account $account = null )
    {

        if (is_string($role)) {
            $roleIdentifier = $role;

            if (in_array($roleIdentifier, array('Everybody', 'Anonymous', 'AuthenticatedUser'))) {
                $roleIdentifier = 'TYPO3.Flow:' . $roleIdentifier;
            }

            if (strpos($roleIdentifier, '.') === false && strpos($roleIdentifier, ':') === false) {
                if ($rolePackageKey === null) {
                    $request = $this->controllerContext->getRequest();
                    $roleIdentifier = $request->getControllerPackageKey() . ':' . $roleIdentifier;
                } else {
                    $roleIdentifier = $rolePackageKey . ':' . $roleIdentifier;
                }
            }

            $role = $this->policyService->getRole($roleIdentifier);
        }

        if ($account instanceof Account) {
            $hasRole = $account->hasRole($role);
        } else {
            $hasRole = $this->securityContext->hasRole($role->getIdentifier());
        }

        if (($hasRole)||((!$hasRole)&&(null === $role))) {

            /** generate the link as originally done in link viewhelper start **/

            $uriBuilder = $this->controllerContext->getUriBuilder();
            if ($useParentRequest) {
                $request = $this->controllerContext->getRequest();
                if ($request->isMainRequest()) {
                    throw new ViewHelper\Exception('You can\'t use the parent Request, you are already in the MainRequest.', 1360163536);
                }
                $uriBuilder = clone $uriBuilder;
                $uriBuilder->setRequest($request->getParentRequest());
            }

            $uriBuilder
                ->reset()
                ->setSection($section)
                ->setCreateAbsoluteUri($absolute)
                ->setArguments($additionalParams)
                ->setAddQueryString($addQueryString)
                ->setArgumentsToBeExcludedFromQueryString($argumentsToBeExcludedFromQueryString)
                ->setFormat($format);
            try {
                $uri = $uriBuilder->uriFor($action, $arguments, $controller, $package, $subpackage);
            } catch (\Exception $exception) {
                throw new ViewHelper\Exception($exception->getMessage(), $exception->getCode(), $exception);
            }
            $this->tag->addAttribute('href', $uri);
            $this->tag->setContent($this->renderChildren());
            $this->tag->forceClosingTag(true);

            return $this->tag->render();

            /** generate the link as originally done in link viewhelper stop **/
        } else {
            /** generate only the link text start **/

            return $this->renderChildren();

            /** generate only the link text stop **/
        }

    }
}

Perhaps someone else can use it, too… :thumbsup:

It might be a good idea not to compare roles but the PrivilegeTargets instead. This will allow you to modify permissions via Policy.yaml without breaking the app.
Furthermore you can use the else or then arguments to spare some code.
In this example this could work like:

<f:security.ifHasAccess privilegeTarget="publicAdminActions" else="Unlinked link text">
  <f:link.action action="edit" arguments="[...]">[Link text here]</f:link.action>
</f:security.ifHasAccess>

Note: To avoid collisions you should consider prefixing your privilege target identifiers such as My.Package:PublicAdminActions

If it is just about MethodPrivileges your custom ViewHelper could even be as clever as to detect whether the target method is allowed. That could look something like this: https://gist.github.com/bwaidelich/77b51054421309f4fb2da8fbeb015dd6

HTH

3 Likes

Hey,

thanks for your reply. Thats quite a cool solution! :thumbsup: