Skip to content

Allow to configure auth access from the resource class #938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 19, 2017

Conversation

dunglas
Copy link
Member

@dunglas dunglas commented Feb 8, 2017

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets api-platform/api-platform#109 (partially), #234
License MIT
Doc PR todo

Edit: now with expression language support

This PR allows to setup authorization controls directly from the resource.

In the following exemple, the only logged in admins can access all operations related to the Secured resource. However, regular users owning a specific resource can also access to it.

All variables available in Symfony's access control expressions are available. The object variable value is the current resource (or collection of resources).

<?php

// src/AppBundle/Entity/Secured.php

namespace AppBundle\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     attributes={"is_granted"="has_role('ROLE_ADMIN')"},
 *     itemOperations={
 *         "get"={"method"="GET", "is_granted"="object.getOwner() == user"}
 *     }
 * )
 * @ORM\Entity
 */
class Secured
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @ORM\Column(type="text")
     */
    public $owner;
}

@dunglas dunglas force-pushed the security branch 3 times, most recently from 24cdab2 to d120dcc Compare February 8, 2017 22:52
@desmax
Copy link
Contributor

desmax commented Feb 10, 2017

Great feature!
But what if you pass to isGranted() not only attribute, but api resource object as well?
This way we could use advantage of symfony voters.

@dunglas
Copy link
Member Author

dunglas commented Feb 10, 2017

@desmax good idea.

@teohhanhui
Copy link
Contributor

(Old suggestion: #234)

I think we should use the Security expression language:

"access_control"="is_granted('ROLE_ADMIN')"

Basically to have the same effect as http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/security.html

@teohhanhui
Copy link
Contributor

And this feature must be disabled when the Symfony Security component is not present. (Additionally, the Symfony Expression component too, if we go with that.)

@dunglas
Copy link
Member Author

dunglas commented Feb 15, 2017

PR updated:

  • Now use expression language
  • Give access to the current object
  • Throw an exception if (and only if) a security rule is used but the security bundle or the expression language component isn't installed

}

if (null === $this->authorizationChecker) {
throw new \LogicException('The "symfony/security" library must be installed to use the "is_granted" attribute.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it worth it to add something like "on property X on class Y" ? to ease debug ? (same for next exception)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


foreach ($isGranted as $expression) {
if ($this->authorizationChecker->isGranted(new Expression($expression), $data)) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is a bit weird. If at least one expression is valid, then it's OK.
What if I want an and between all my expressions? And In order to avoid dealing with this kind of decission, why not allowing only one is_granted?

(Actually, I don't know, I'm just asking)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this is a rest of the old logic (without expression language) but it's better to only allow one expression now.

I'll update the PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@dunglas dunglas force-pushed the security branch 2 times, most recently from 13f4922 to ab0a07f Compare February 16, 2017 11:12
@wadjeroudi
Copy link

@dunglas Thanks for your work. This feature will be released on next version or do we need to use dev-master ?

@dunglas
Copy link
Member Author

dunglas commented Apr 7, 2017

It will be in 2.1.

@piotrbrzezina
Copy link

piotrbrzezina commented Apr 20, 2017

Hi
@dunglas is there any way to use this solution with custom voters ?

attributes={"is_granted"="is_granted('view',object)"}

@desmax
Copy link
Contributor

desmax commented Apr 20, 2017

@piotrbrzezina define your operation smth like this "get"={"method"="GET", "is_granted"="get"},
then register voter, that supports that class, but give it a priority 246, so it would run before ExpressionVoter and after AuthenticatedVoter.

@larkarvin
Copy link

I have implemented mine as

/**
 * @ApiResource(
 *     attributes={"is_granted"="has_role('ROLE_ADMIN')"},
 *     itemOperations={
 *         "get"={"method"="GET", "is_granted"="object.isAllowed(user)"}
 *     }
 * )
 * @ORM\Entity
 */
class Book
{
    // code omitted
    public function isAllowed($user)
    {
        if($user->hasRole('ROLE_ADMIN')){
            return TRUE;
        }

        if($this->getOwner() == $user){
            return TRUE;
        }
        return FALSE;
    }
}

I needed the extra check that if the logged user is an admin, they will be allowed to access the item operation.

@wcoppens
Copy link
Contributor

wcoppens commented Jun 15, 2017

@desmax Thanks for the info. It seems that using it the way you suggest makes it possible to go through the custom security voter, however the get in your case is still being seen as a expression by Symfony and so finally goes through the ExressionVoter in which it can't parse the expression because it's invalid.

Any suggestion how we can prevent this becoming a Expression?

//EDIT

It seems the DenyAccessListener class does this at the end:

if (!$this->authorizationChecker->isGranted(new Expression($isGranted), $request->attributes->get('data'))) {
    throw new AccessDeniedException();
}

Perhaps we should find a way to check whether the $isGranted variable is a true expression?

@eloyekunle
Copy link

eloyekunle commented Jul 31, 2017

Also, there's some useful stuff at https://api-platform.com/docs/core/extensions.
There's an example which includes extending the Doctrine queries to add or remove information about collection operations or item operations. In the example, access control was used to explain it.

@bitcoinporn
Copy link

bitcoinporn commented Aug 3, 2017

I implementing complex authorization, i have 4 level configuration:

  1. Basic | nothing to do here
  2. Medium | i configure via attributes base on access level.
*      attributes={
*          "authorization" = "api.firewall.comment",
*          "permissions" = {
*                  "collection" = ["GUEST", "ROLE_ADMIN"],
*                  "item" = ["GUEST", "ROLE_ADMIN"],
*                  "create" = ["GUEST", "ROLE_ADMIN"],
*                  "update" = ["GUEST", "ROLE_ADMIN"],
*                  "delete" = ["GUEST", "ROLE_ADMIN"],
*                  "getCollection" = ["GUEST", "ROLE_ADMIN"],
*          },
  1. Custom Authorization based Roles, Entity & Operations.
  2. Custom Authorization based on Role and Object. Like "ROLE_USER" only able change status

My solutions is creating custom event subscriber, then define "authorization" class on which @ApiResource definition.

I create specific class for each entity who need custom authorization:

services:
    api.firewall.voter:
        class: Tstudio\ApiBundle\Firewall\Pool
        arguments: ['@api_platform.metadata.resource.metadata_factory', '@service_container']
        tags:
            - { name: kernel.event_subscriber}

    api.firewall.comment:
        class: Tstudio\ApiBundle\Firewall\Voter\Comment
        tags:
            - { name: api.firewall.entity }
        

And inject class into custom event subscriber class via addMethod call:

$firewall = $container->getDefinition('api.firewall.voter');
       foreach ($container->findTaggedServiceIds('api.firewall.entity') as $id => $attributes)
       {
           $firewall->addMethodCall('addVoter', array($id, new Reference($id)));
       }

it work great and flexible. I think we can implement this into core system

@ksom
Copy link

ksom commented Oct 21, 2017

@larkarvin What do you think about:

/**
 * @ApiResource(
 *     attributes={"is_granted"="has_role('ROLE_ADMIN')"},
 *     itemOperations={
 *         "get"={"method"="GET", "is_granted"="has_role('ROLE_ADMIN') || object.getOwner()
 == user"}
 *     }
 * )
 * @ORM\Entity
 */
class Book
{
}

hoangnd25 pushed a commit to hoangnd25/core that referenced this pull request Feb 23, 2018
Allow to configure auth access from the resource class
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.