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
Conversation
for more information, see https://pre-commit.ci
…into boolean-expression-permissions # Conflicts: # strawberry/permission.py
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
…into boolean-expression-permissions
for more information, see https://pre-commit.ci
…ons as base class. Branch on composite permissions to handle them differently!
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
for more information, see https://pre-commit.ci
…into boolean-expression-permissions
Thanks for adding the Here's a preview of the changelog: Adds the ability to use the 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:
|
There was a problem hiding this 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
Help me be more useful! Please click 👍 or 👎 on each comment to tell me if it was helpful.
strawberry/permission.py
Outdated
@@ -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)]]: |
There was a problem hiding this comment.
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.
) -> Union[bool, Awaitable[bool], (False, dict), Awaitable[(False, dict)]]: | |
) -> Union[bool, Awaitable[bool]]: |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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.
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(): |
There was a problem hiding this comment.
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.
for permission in self.child_permissions: | ||
if not permission.has_permission(source, info, **kwargs): | ||
return False, {"failed_permissions": [permission]} | ||
return True |
There was a problem hiding this comment.
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
)
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, | |
) |
Codecov ReportAttention: Patch coverage is
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 |
CodSpeed Performance ReportMerging #3451 will not alter performanceComparing Summary
|
return True | ||
failed_permissions.append(permission) | ||
|
||
return False, {"failed_permissions": failed_permissions} |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)? 😅
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
|
||
def _on_unauthorized(self, permission: BasePermission) -> Any: | ||
def _on_unauthorized(self, permission: BasePermission, **kwargs: dict) -> Any: |
There was a problem hiding this comment.
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 🙂
There was a problem hiding this comment.
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
Co-authored-by: Doctor <[email protected]>
Let's keep working in #3408 😊 |
Supersedes #3408