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

feat: add boolean permission support #3451

Closed
wants to merge 33 commits into from

Conversation

erikwrede
Copy link
Member

@erikwrede erikwrede commented Apr 14, 2024

Supersedes #3408

Nathan John and others added 30 commits March 14, 2024 14:55
…into boolean-expression-permissions

# Conflicts:
#	strawberry/permission.py
…ons as base class. Branch on composite permissions to handle them differently!
@botberry
Copy link
Member

botberry commented Apr 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 Erik Wrede for the PR 👏

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

Copy link

@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 @erikwrede - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟢 Security: all looks good
  • 🟡 Testing: 4 issues found
  • 🟢 Complexity: all looks good
  • 🟢 Docstrings: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment to tell me if it was helpful.

@@ -51,17 +52,35 @@ class BasePermission(abc.ABC):
@abc.abstractmethod
def has_permission(
self, source: Any, info: Info, **kwargs: Any
) -> Union[bool, Awaitable[bool]]:
) -> Union[bool, Awaitable[bool], (False, dict), Awaitable[(False, dict)]]:
Copy link

Choose a reason for hiding this comment

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

suggestion (code_refinement): Consider simplifying the return type for better maintainability.

The method now has multiple return types, which could complicate the handling of its output. Simplifying to a consistent return type, possibly encapsulated within a class or structure, might improve maintainability and readability.

Suggested change
) -> Union[bool, Awaitable[bool], (False, dict), Awaitable[(False, dict)]]:
) -> Union[bool, Awaitable[bool]]:

Copy link
Contributor

Choose a reason for hiding this comment

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

By the way, this looks like a mistake (a, b) is not a valid type 🤔 Perhaps you need Tuple[Literal[False], dict]?

@@ -54,6 +54,470 @@ def user(self) -> str: # pragma: no cover
assert result.errors[0].message == "User is not authenticated"


@pytest.mark.asyncio
async def test_no_graphql_error_when_and_permission_is_allowed():
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding tests for mixed async and sync permission combinations.

It would be beneficial to verify the behavior when permissions are mixed (some async, some sync) in the AND and OR scenarios to ensure the system handles these mixed cases correctly.

@@ -54,6 +54,470 @@
assert result.errors[0].message == "User is not authenticated"


@pytest.mark.asyncio
async def test_no_graphql_error_when_and_permission_is_allowed():
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Add negative test cases for async permission checks.

While there are tests for successful permission checks, adding negative test cases where async permissions fail would help ensure robust error handling and behavior verification.

@@ -54,6 +54,470 @@
assert result.errors[0].message == "User is not authenticated"


@pytest.mark.asyncio
async def test_no_graphql_error_when_and_permission_is_allowed():
Copy link

Choose a reason for hiding this comment

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

suggestion (testing): Verify handling of exceptions thrown by permission checks.

It's important to ensure that any exceptions thrown during permission checks are handled gracefully and do not cause unexpected crashes or behavior.

Suggested change
async def test_no_graphql_error_when_and_permission_is_allowed():
try:
result = schema.execute_sync(query)
assert result.data["user"] == "patrick"
except Exception as e:
assert False, f"Unexpected error occurred: {str(e)}"

@@ -54,6 +54,470 @@
assert result.errors[0].message == "User is not authenticated"


@pytest.mark.asyncio
async def test_no_graphql_error_when_and_permission_is_allowed():
Copy link

Choose a reason for hiding this comment

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

nitpick (testing): Ensure consistency in the use of 'pragma: no cover'.

The use of 'pragma: no cover' is noted in several test cases. Please ensure it is used consistently across all test cases where applicable to maintain coverage clarity.

Comment on lines +146 to +149
for permission in self.child_permissions:
if not permission.has_permission(source, info, **kwargs):
return False, {"failed_permissions": [permission]}
return True
Copy link

Choose a reason for hiding this comment

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

suggestion (code-quality): Use the built-in function next instead of a for-loop (use-next)

Suggested change
for permission in self.child_permissions:
if not permission.has_permission(source, info, **kwargs):
return False, {"failed_permissions": [permission]}
return True
return next(
(
(False, {"failed_permissions": [permission]})
for permission in self.child_permissions
if not permission.has_permission(source, info, **kwargs)
),
True,
)

@erikwrede erikwrede mentioned this pull request Apr 14, 2024
3 tasks
Copy link

codecov bot commented Apr 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.49%. Comparing base (0c5bc4b) to head (29fe77f).
Report is 29 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3451      +/-   ##
==========================================
+ Coverage   96.47%   96.49%   +0.02%     
==========================================
  Files         509      510       +1     
  Lines       32691    33052     +361     
  Branches     5399     5478      +79     
==========================================
+ Hits        31539    31894     +355     
+ Misses        941      921      -20     
- Partials      211      237      +26     

Copy link

codspeed-hq bot commented Apr 14, 2024

CodSpeed Performance Report

Merging #3451 will not alter performance

Comparing boolean-expression-permissions-erik (29fe77f) with main (17f05a7)

Summary

✅ 12 untouched benchmarks

return True
failed_permissions.append(permission)

return False, {"failed_permissions": failed_permissions}
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably get rid of dictionaries here, maybe make a dataclass instead 🤔

Copy link
Member Author

@erikwrede erikwrede Apr 14, 2024

Choose a reason for hiding this comment

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

The idea here was to remain as flexible as possible, allowing users to pass context in the future as well. With a dataclass we would be again very static about handling additional permission error context. Maybe a TypedDict can help improve our defaults for this particular case?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, I'm not sure how context would be used here, is it in the release noted (that I didn't read)? 😅

Copy link
Member Author

@erikwrede erikwrede May 3, 2024

Choose a reason for hiding this comment

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

It's internal for now, but it allows us to pass information to on_unauthorized from has_permission. We opted to separate exception raising from permission checking back when implementing this extension due to the fail silently feature.

Copy link
Member Author

Choose a reason for hiding this comment

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

So basically the context contains "this permission failed out of the list of and / or permissions"

strawberry/permission.py Outdated Show resolved Hide resolved

def _on_unauthorized(self, permission: BasePermission) -> Any:
def _on_unauthorized(self, permission: BasePermission, **kwargs: dict) -> Any:
Copy link
Contributor

Choose a reason for hiding this comment

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

This is probably a mistake - with kwargs you annotate value type (e.g. int instead of dict[str, int], not sure what was intended here 🙂

Copy link
Contributor

Choose a reason for hiding this comment

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

If there are no particular operations performed on kwargs values you could annotate them as object, as it's the most narrow type, Any being worse for that use case

@patrick91
Copy link
Member

Let's keep working in #3408 😊

@patrick91 patrick91 closed this May 16, 2024
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.

None yet

5 participants