Skip to content

Implement ApiProperty security attribute#3227

Closed
fredericbarthelet wants to merge 1 commit into
api-platform:masterfrom
fredericbarthelet:use-api-property-for-attribute-acl-computation
Closed

Implement ApiProperty security attribute#3227
fredericbarthelet wants to merge 1 commit into
api-platform:masterfrom
fredericbarthelet:use-api-property-for-attribute-acl-computation

Conversation

@fredericbarthelet

@fredericbarthelet fredericbarthelet commented Oct 31, 2019

Copy link
Copy Markdown
Contributor
Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tickets fixes #773
License MIT
Doc PR api-platform/docs#1000

Implement new security attribute on ApiProperty annotations.
Allows built-in support for property removal from resource using acl expression language.

This initial PR is a POC to explain my intention :

  • adding new ApiProperty attribute security
  • injecting ResourceAccessChecker within AbstractItemNormalizer
  • supercharge isAllowedAttribute method inherited from symfony AbstractNormalizer service
  • early return based on acl resolution if resolved to false

WDYT ?

@fredericbarthelet fredericbarthelet force-pushed the use-api-property-for-attribute-acl-computation branch from f53b219 to 071b71b Compare October 31, 2019 16:03
@fredericbarthelet fredericbarthelet changed the title WIP Implement ApiProperty security attribute Oct 31, 2019
@fredericbarthelet fredericbarthelet force-pushed the use-api-property-for-attribute-acl-computation branch from 071b71b to 65cbde8 Compare October 31, 2019 16:05
@soyuka

soyuka commented Nov 1, 2019

Copy link
Copy Markdown
Member

I find this interesting, @dunglas is this what you had in mind?

@antograssiot antograssiot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

doesn't the api_platform.jsonld.normalizer.item, api_platform.jsonapi.normalizer.item and api_platform.hal.normalizer.item services declaration needs to be updated as well for this to work in real apps ?

protected $localCache = [];

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ResourceAccessCheckerInterface $resourceAccessChecker, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Additional arguments must be added at the end with and with a default value or it is a BC break

return false;
}

return parent::isAllowedAttribute($classOrObject, $attribute, $format, $context);

@teohhanhui teohhanhui Nov 1, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The parent method should be called first. No need to do extra work when we can return the value directly if it's already false. We only need to check against the security attribute when the parent method returns true, since we're taking a blacklist approach here.

* @ORM\Column
* @Assert\NotBlank
* @ApiProperty(iri="http://schema.org/name")
* @ApiProperty(iri="http://schema.org/name", security="is_granted('ROLE_USER')")

@teohhanhui teohhanhui Nov 1, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please don't make this change. I suspect this will affect many existing tests. The new property you've added ($adminOnlyProperty) is enough.

@teohhanhui

Copy link
Copy Markdown
Contributor

Just to provide some background: @fredericbarthelet discussed this with me during the Hackday in Paris.

Thanks for following up on this!

@fredericbarthelet fredericbarthelet force-pushed the use-api-property-for-attribute-acl-computation branch 5 times, most recently from 67cf534 to c56fe21 Compare November 4, 2019 19:57
* @ORM\Column(nullable=true)
* @ApiProperty(security="is_granted('ROLE_ADMIN')")
*/
private $adminOnlyProperty;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the same changes must be done in tests/Fixtures/TestBundle/Document/Dummy.php so the mongoDB suite will pass too

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a suggestion, but using another Entity/Document will avoid to have to modify filters tests

Maybe this could be added to the SecuredDummy instead, it will have less side impacts and will be at the same place than the security attribute from ApiResource

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@antograssiot that what I noticed as well when I started implementing behat tests. I will move it to SecuredDummy instead :)

@fredericbarthelet fredericbarthelet force-pushed the use-api-property-for-attribute-acl-computation branch 5 times, most recently from 7bffc91 to 4bcb395 Compare November 7, 2019 11:03
@fredericbarthelet

fredericbarthelet commented Nov 7, 2019

Copy link
Copy Markdown
Contributor Author

@soyuka, @antograssiot, @teohhanhui I updated my PR to take into account your comments. Thank you for your feedbacks :) !

I encountered an issue when integrated security property on graphQL API.
When a user makes a graphQL request with a field he cannot access based on this new security property, ie :

Resource

    /**
     * @ApiProperty(security="is_granted('ROLE_ADMIN')")
     */
    private $adminOnlyProperty = '';

Request

    {
      securedDummy(id: "/secured_dummies/2") {
        owner
        title
        adminOnlyProperty
      }
    }

I get a 200 HTTP answer with an Internal Server Error message because Cannot return null for non-nullable field SecuredDummy.adminOnlyProperty. IMHO, the ideal feedback message should be similar to the one returned when requesting non-existing properties, aka Cannot query field \"adminOnlyProperty\" on type \"SecuredDummy\".

My idea would be to inject as well ResourceAccessCheckerInterface service in src/GraphQl/Type/FieldsBuilder.php and modify generated fields on graphQL allowed operations to remove those removed by the same isAllowedAttribute method of the ItemNormalizer. This is done in the getResourceObjectTypeFields method. Do you agree with such solution ?

@fredericbarthelet fredericbarthelet force-pushed the use-api-property-for-attribute-acl-computation branch 2 times, most recently from 49ad651 to 28b4d38 Compare November 7, 2019 16:14
@fredericbarthelet fredericbarthelet force-pushed the use-api-property-for-attribute-acl-computation branch from 28b4d38 to 2aa51f9 Compare November 16, 2019 19:18
@alanpoulain

Copy link
Copy Markdown
Member

@fredericbarthelet I'm not sure it's a good idea to check the security inside the FieldsBuilder. The GraphQL system has two steps:

  • building the schema
  • resolving a query

The built schema should not be dynamic and dependent on the query. That's why you can remove fields for instance by using non-dynamic normalization context but not in your case IMO.

I'm not sure it's the best way, but my proposal would be to make the field nullable (or to throw an exception to force the user to make it nullable?) if it has a security attribute and set it to null if the user doesn't have the rights to see the value of this field.

@teohhanhui

teohhanhui commented Nov 18, 2019

Copy link
Copy Markdown
Contributor

my proposal would be to make the field nullable (or to throw an exception to force the user to make it nullable?) if it has a security attribute and set it to null if the user doesn't have the rights to see the value of this field

That does not make sense... :x We should find a better solution.

@alanpoulain

Copy link
Copy Markdown
Member

From what I read in the GraphQL examples, it seems to be the current practice: https://graphql.org/learn/authorization/

@teohhanhui

Copy link
Copy Markdown
Contributor

I don't see the relevance of the link you've shared. I'm not advocating doing it at the GraphQL layer. Making fields nullable is propagating this authorization logic to the GraphQL layer, which is exactly what they've warned against.

@alanpoulain

Copy link
Copy Markdown
Member

In most GraphQL systems, the schema cannot change dynamically depending on the context (very often it's a static file). So you have to make sure the fields can be nullable if you want some users to not see the corresponding value.

That's also why in the type system, the types are nullable by default.

If you take the GitHub API (https://developer.github.com/v4), 95% of the fields are nullable.

@alanpoulain

Copy link
Copy Markdown
Member

See also: graphql/graphql-js#113

@alanpoulain

Copy link
Copy Markdown
Member

GraphQL Ruby seems to be the only one doing it. I'm not sure we should too. https://graphql-ruby.org/authorization/visibility

@teohhanhui

Copy link
Copy Markdown
Contributor

That sounds like a good strategy, yes. Let's get this feature for REST first.

@alanpoulain

Copy link
Copy Markdown
Member

I think it will be good to be coherent with GraphQL too...
For me there are two choices:

  • force the field to be nullable in the GraphQL schema if it has the security attribute
  • throw an exception if the field is not nullable if it has a security attribute

The @api-platform/core-team should make a choice and not just leave the problem because it's GraphQL.

@alanpoulain

Copy link
Copy Markdown
Member

I've added an issue for the visibility attribute too: #3279.

@teohhanhui

teohhanhui commented Nov 20, 2019

Copy link
Copy Markdown
Contributor

I agree that it should be supported for GraphQL too, but it needs more work in the GraphQL system of API Platform before that's possible. We don't need to block this PR because of that.

The nullable proposal is not feasible, per discussion above.

@alanpoulain

Copy link
Copy Markdown
Member

The nullable property is totally feasible.
If you refer to:

Making fields nullable is propagating this authorization logic to the GraphQL layer, which is exactly what they've warned against.

I think you misunderstood what they say. First of all, they are talking about the resolver part of the GraphQL system. We are talking about the building part here. Secondly, it's only a recommendation to avoid duplication. Here we can do this with no issue like this. And finally the "problem" would be the same for the visibility attribute.

Now that the GraphQL system is introduced and is fully working, we should make sure new features are working for it too.

@teohhanhui

Copy link
Copy Markdown
Contributor

#3227 (comment)

@alanpoulain

Copy link
Copy Markdown
Member

I've already responded to this comment.
Yes you cannot distinguish between a "true" null value and a null value because it's not available for you.
But that's how GraphQL has been thought. Even if you don't agree with this (which I can understand), it's working like this today in most GraphQL systems. We are not here to question the GraphQL specification.
And if the user has a problem with it, the visibility attribute could solve it.
That's how GraphQL Ruby is doing it for instance: https://graphql-ruby.org/authorization/authorization#handling-unauthorized-objects

@teohhanhui

teohhanhui commented Nov 20, 2019

Copy link
Copy Markdown
Contributor

So yes, if you insist that GraphQL support needs to be implemented in this PR (which I disagree with), then we're now blocked by #3279

What I'm saying is, we can add GraphQL support for this feature once #3279 is implemented. There's no need to block this contribution now.

@alanpoulain

Copy link
Copy Markdown
Member

Blocked? No we're not. This visibility attribute can be done in a second time (or even never).
I've proposed two solutions above to solve the GraphQL part. Both have advantages and drawbacks but we just need to make a choice.

@teohhanhui

teohhanhui commented Nov 20, 2019

Copy link
Copy Markdown
Contributor

Do you have examples of other GraphQL implementations which return null on unauthorized access? I'm not convinced that it's a feasible alternative. From the discussions I've seen, it's either no authorization on property level, or implement the visibility feature.

@alanpoulain

Copy link
Copy Markdown
Member

@theofidry

theofidry commented Nov 20, 2019 via email

Copy link
Copy Markdown
Contributor

@alanpoulain

Copy link
Copy Markdown
Member

Some GraphQL systems have not implemented the visibility feature yet.
@theofidry having an error entry could be a third solution. I don't really like it because it's not really an error for me but we can discuss it.

@theofidry

theofidry commented Nov 20, 2019 via email

Copy link
Copy Markdown
Contributor

@theofidry

theofidry commented Nov 20, 2019 via email

Copy link
Copy Markdown
Contributor

@fredericbarthelet fredericbarthelet force-pushed the use-api-property-for-attribute-acl-computation branch 2 times, most recently from b1f159a to 399ab49 Compare November 20, 2019 17:29
@fredericbarthelet

Copy link
Copy Markdown
Contributor Author

I updated my PR, removing the implementation I had for GraphQL part for now.

@alanpoulain I would be happy to help implement the desired strategy on graphQL in a seperate PR related to the newly opened issue #3279 if that's OK for you :)

Any remaining comments on rest part on this one @teohhanhui ?

@fredericbarthelet fredericbarthelet force-pushed the use-api-property-for-attribute-acl-computation branch from 399ab49 to 5f1bbf1 Compare November 27, 2019 14:16
@GregoireHebert

Copy link
Copy Markdown
Contributor

I like this, could you also add tests for writing operations?

@dunglas dunglas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry for the late review, but it's awesome!

@chalasr

chalasr commented Apr 21, 2020

Copy link
Copy Markdown
Contributor

Finished in #3503, can be closed.

@soyuka soyuka closed this Apr 27, 2020
@soyuka

soyuka commented Apr 27, 2020

Copy link
Copy Markdown
Member

thanks @fredericbarthelet

@dunglas

dunglas commented Nov 9, 2020

Copy link
Copy Markdown
Member

For the record, I'm also in favor of returning null instead of throwing an error when a property cannot be accessed because of a security rule in the GraphQL subsystem.

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.

9 participants