Skip to content
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

Boolean expression permissions #3408

Open
wants to merge 38 commits into
base: main
Choose a base branch
from

Conversation

vethan
Copy link
Contributor

@vethan vethan commented Mar 14, 2024

Adding the ability to perform boolean expressions on permissions using the permission extension

Description

Allows permissions to be supplied in the form IsAuthed() & (ControlsAccount() | IsAdmin()), by overriding the implementation of permissions on BasePermission

Types of Changes

  • Core
  • Bugfix
  • [ x] New feature
  • [x ] Enhancement/optimization
  • Documentation

Issues Fixed or Closed by This PR

Checklist

  • [x ] My code follows the code style of this project.
  • [ x] My change requires a change to the documentation.
  • [ x] I have updated the documentation accordingly.
  • [x ] I have read the CONTRIBUTING document.
  • [ x] I have added tests to cover my changes.
  • [ x] I have tested the changes and verified that they work and don't break anything (as well as I can manage).

@botberry
Copy link
Member

botberry commented Mar 14, 2024

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


Adds the ability to use the & and | operators on permissions to form boolean logic. For example, if you want
a field to be accessible with either the IsAdmin or IsOwner permission you
could define the field as follows:

import strawberry
from strawberry.permission import PermissionExtension, BasePermission


@strawberry.type
class Query:
    @strawberry.field(
        extensions=[
            PermissionExtension(
                permissions=[(IsAdmin() | IsOwner())], fail_silently=True
            )
        ]
    )
    def name(self) -> str:
        return "ABC"

Here's the tweet text:

🆕 Release (next) is out! Thanks to @vethan4 for the PR 👏

Get it here 👉 https://strawberry.rocks/release/(next)

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @vethan - I've reviewed your changes and they look great!

General suggestions:

  • Consider adding more detailed examples in the documentation to cover complex use cases and best practices for using boolean expressions with permissions.
  • Ensure that the performance implications of the new boolean permission checks are well understood and documented, especially for applications with high throughput.
  • Review the exception handling strategy, particularly the use of BaseException, to ensure that it aligns with best practices and does not catch more than intended.
  • Clarify the behavior and implications of the fail_silently option in the documentation to prevent any unexpected behavior from client perspectives.
Here's what I looked at during the review
  • 🟡 General issues: 4 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟢 Complexity: all looks good
  • 🟢 Docstrings: all looks good

Thanks for using Sourcery. We offer it for free for open source projects and would be very grateful if you could help us grow. If you like it, would you consider sharing Sourcery on your favourite social media? ✨

Share Sourcery

Help me be more useful! Please click 👍 or 👎 on each comment to tell me if it was helpful.

strawberry/permission.py Outdated Show resolved Hide resolved
strawberry/permission.py Show resolved Hide resolved
Copy link

codecov bot commented Mar 14, 2024

Codecov Report

Attention: Patch coverage is 96.86275% with 8 lines in your changes are missing coverage. Please review.

Project coverage is 96.51%. Comparing base (fc322a8) to head (901abf0).

❗ Current head 901abf0 differs from pull request most recent head 64bcbad. Consider uploading reports for the commit 64bcbad to get more accurate results

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #3408    +/-   ##
========================================
  Coverage   96.51%   96.51%            
========================================
  Files         511      511            
  Lines       32965    33209   +244     
  Branches     5482     5517    +35     
========================================
+ Hits        31815    32051   +236     
- Misses        917      921     +4     
- Partials      233      237     +4     

Copy link

codspeed-hq bot commented Mar 14, 2024

CodSpeed Performance Report

Merging #3408 will not alter performance

Comparing vethan:boolean-expression-permissions (64bcbad) with main (fc322a8)

Summary

✅ 12 untouched benchmarks

@ThirVondukr
Copy link
Contributor

Do we need to have new public methods resolve_permission_async and resolve_permission_sync?
You can just return a bool | Awaitable[bool] from has_permission based on if any of the underlying permissions are actually async, and I don't think you need to have is_async property either
It would be a good thing to not extend public API without a need for that 🤔

@vethan
Copy link
Contributor Author

vethan commented Mar 14, 2024

Do we need to have new public methods resolve_permission_async and resolve_permission_sync?

This may just be my lack of pythonic syntax knowledge creeping up, but I'm not sure if I can return the Union from the parent class without the knowledge of the children's return type. (Aka, does the AND need to await to figure out it's own permission or not)

Copy link
Member

@erikwrede erikwrede left a comment

Choose a reason for hiding this comment

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

Thanks for the initial version! I have a few comments but am short on time, thats why theyre very short. I'll revisit this in a few days 🙂

strawberry/permission.py Outdated Show resolved Hide resolved
strawberry/permission.py Outdated Show resolved Hide resolved
strawberry/permission.py Outdated Show resolved Hide resolved
strawberry/permission.py Outdated Show resolved Hide resolved
strawberry/permission.py Outdated Show resolved Hide resolved
Comment on lines 59 to 65
def resolve_permission_sync(self, source: Any, info: Info, **kwargs: Any) -> None:
if self.has_permission(source, info, **kwargs):
return None
else:
return self.on_unauthorized()

async def resolve_permission_async(
Copy link
Member

Choose a reason for hiding this comment

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

Splitting into sync and async is a breaking change, I'd like to avoid this. We can think about a different solution :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it seemed a bit gross, very open to a better solution. As mentioned, might be my lack of Python Syntax knowledge here!

Copy link
Contributor

Choose a reason for hiding this comment

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

@vethan Sorry for that, but could you probably adapt some of the stuff from my implementation on discord? 🤔
https://discord.com/channels/689806334337482765/741575032257511485/1217820662245101609

If I remember correctly it should work with both async and sync by inspecting whenever any of child permissions are async

Copy link
Contributor

Choose a reason for hiding this comment

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

Also if applied operator is the same as the one in PermissionHolder I think you could avoid nesting them too by returning something like

PermissionHolder(operator=..., permissions=[*self.permissions, other])

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Your implementation was great for has_permission, but then there was no way to work through on_unauthorised correctly unfortunately :/ Need to split the logic a bit for that

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, on_unauthorised is a bit hard to implement because we don't know which permission didn't succeed without checking them again (and we can't even do that)

strawberry/permission.py Show resolved Hide resolved


class CompositePermission(BasePermission, abc.ABC):
child_permissions: List[BasePermission]
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove this annotation, this basically lies to type checker that this property would always be present on a class.
Your __init__ is enough to provide that type information

for index, permission in enumerate(self.child_permissions):
if not permission.has_permission(source, info, **kwargs):
return False, index
return True, 0
Copy link
Contributor

@ThirVondukr ThirVondukr Apr 11, 2024

Choose a reason for hiding this comment

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

Maybe it would be a better idea to return failed permission and not it's index?

True
for permission in self.permissions
if iscoroutinefunction(permission.has_permission)
True for permission in self.permissions if permission.is_async
]
return len(async_permissions) == 0
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return len(async_permissions) == 0
return all(not permission.is_async for permission in self.permissions)

@erikwrede
Copy link
Member

Hey everyone, I worked on streamlining this with @patrick91 over the last days. New version is in #3451. Please comment there! @vethan thanks for the solid ground work. I just reduced the complexity a bit in some places but essentially it's your PR. The only missing thing is some schema directive support. We have decided to deprecate the old way of handling Schema Directives in favor of a new Permission directive containing custom info listing all the required permissions for a field.

@patrick91
Copy link
Member

@erikwrede maybe you can push those commits on this PR? do you have permission? 😊

@patrick91
Copy link
Member

/pre-release

@botberry
Copy link
Member

botberry commented May 16, 2024

Pre-release

👋

Pre-release 0.229.2.dev.1715881453 [64bcbad] has been released on PyPi! 🚀
You can try it by doing:

poetry add strawberry-graphql==0.229.2.dev.1715881453

@strawberry-graphql strawberry-graphql deleted a comment from botberry May 16, 2024
@erikwrede
Copy link
Member

erikwrede commented May 16, 2024

Just pushed some additional fixes from local regarding context passing with multiple nested boolean permissions

@patrick91
Copy link
Member

/pre-release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants