Repository navigation
fix: enhance permission checks by adding user group resource permissions #7857
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,7 +40,7 @@ | |
| from knowledge.services import validate_knowledge_file_size | ||
| from maxkb.const import CONFIG | ||
| from rest_framework import serializers | ||
| from system_manage.models import ChatUserApiKey, WorkspaceUserResourcePermission | ||
| from system_manage.models import ChatUserApiKey, WorkspaceUserGroupResourcePermission, WorkspaceUserResourcePermission | ||
| from system_manage.models.resource_mapping import ResourceMapping, ResourceType | ||
| from tools.serializers.tool import UploadedFileField | ||
| from users.models import User | ||
|
|
@@ -417,7 +417,7 @@ def _check_workspace_resource_permission( | |
| else ["VIEW", "MANAGE"] | ||
| ) | ||
| if ( | ||
| not QuerySet(WorkspaceUserResourcePermission) | ||
| QuerySet(WorkspaceUserResourcePermission) | ||
| .filter( | ||
| target=target_id, | ||
| workspace_id=workspace_id, | ||
|
|
@@ -427,7 +427,20 @@ def _check_workspace_resource_permission( | |
| ) | ||
| .exists() | ||
| ): | ||
| _deny() | ||
| return | ||
| if ( | ||
| QuerySet(WorkspaceUserGroupResourcePermission) | ||
| .filter( | ||
| target=target_id, | ||
| workspace_id=workspace_id, | ||
| user_group__user_relations__user_id=user_id, | ||
| auth_target_type=auth_target_type, | ||
| permission_list__overlap=permission_list, | ||
| ) | ||
| .exists() | ||
| ): | ||
| return | ||
| _deny() | ||
|
|
||
|
|
||
| class FileSerializer(serializers.Serializer): | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The provided code change introduces significant logic errors and performance issues. Below is a detailed analysis: 1. Critical Logic Error: Inverted Permission CheckLocation:
2. Performance Issue: N+1 Query / Double Database HitLocation: The two separate
3. Potential Bug:
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,7 +9,7 @@ | |||||||||||||||||||||
| from common.exception.app_exception import AppUnauthorizedFailed | ||||||||||||||||||||||
| from knowledge.models import Document, File, FileSourceType, Knowledge, Paragraph | ||||||||||||||||||||||
| from oss.serializers.file import FileSerializer, _auth_system, _check_workspace_resource_permission | ||||||||||||||||||||||
| from system_manage.models import WorkspaceUserResourcePermission | ||||||||||||||||||||||
| from system_manage.models import WorkspaceUserGroupResourcePermission, WorkspaceUserResourcePermission | ||||||||||||||||||||||
| from users.models import User | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -19,6 +19,9 @@ def setUp(self): | |||||||||||||||||||||
| self.grants = MagicMock() | ||||||||||||||||||||||
| self.grants.filter.return_value = self.grants | ||||||||||||||||||||||
| self.grants.exists.return_value = False | ||||||||||||||||||||||
| self.group_grants = MagicMock() | ||||||||||||||||||||||
| self.group_grants.filter.return_value = self.group_grants | ||||||||||||||||||||||
| self.group_grants.exists.return_value = False | ||||||||||||||||||||||
| self.user_query = MagicMock() | ||||||||||||||||||||||
| self.user_query.filter.return_value.first.return_value = SimpleNamespace(id="user") | ||||||||||||||||||||||
| self.document_query = MagicMock() | ||||||||||||||||||||||
|
|
@@ -32,6 +35,7 @@ def setUp(self): | |||||||||||||||||||||
| Paragraph: self.document_query, | ||||||||||||||||||||||
| Knowledge: self.knowledge_query, | ||||||||||||||||||||||
| WorkspaceUserResourcePermission: self.grants, | ||||||||||||||||||||||
| WorkspaceUserGroupResourcePermission: self.group_grants, | ||||||||||||||||||||||
| }[model] | ||||||||||||||||||||||
| self.enterContext(patch("oss.serializers.file.get_auth", return_value=self.auth)) | ||||||||||||||||||||||
| self.file = File(source_type=FileSourceType.DOCUMENT, source_id="document") | ||||||||||||||||||||||
|
|
@@ -73,6 +77,17 @@ def test_explicit_view_and_manage_grants_allow_source_file_reads(self): | |||||||||||||||||||||
| permission_list__overlap=["VIEW", "MANAGE"], | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def test_user_group_view_and_manage_grants_allow_source_file_reads(self): | ||||||||||||||||||||||
| self.group_grants.exists.return_value = True | ||||||||||||||||||||||
| _auth_system(self.file, "user") | ||||||||||||||||||||||
| self.group_grants.filter.assert_called_once_with( | ||||||||||||||||||||||
| target="knowledge", | ||||||||||||||||||||||
| workspace_id="ws", | ||||||||||||||||||||||
| user_group__user_relations__user_id="user", | ||||||||||||||||||||||
| auth_target_type="KNOWLEDGE", | ||||||||||||||||||||||
| permission_list__overlap=["VIEW", "MANAGE"], | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def test_custom_workspace_manager_uses_the_permission_object(self): | ||||||||||||||||||||||
| self.auth.roles = {"EXTENDS_WORKSPACE_MANAGE:w:ws"} | ||||||||||||||||||||||
| self.auth.permissions = {"KNOWLEDGE_WORKSPACE_MANAGE:w:ws": PermissionConstants.KNOWLEDGE_READ.value.bit()} | ||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is a review of the provided code diff, focusing on test logic, mocking strategy, and potential issues. 1. Mocking Strategy & Isolation (Potential Issue)The most significant concern is how
2. Test Assertion SpecificityIn self.group_grants.filter.assert_called_once_with(
target="knowledge",
workspace_id="ws",
user_group__user_relations__user_id="user",
auth_target_type="KNOWLEDGE",
permission_list__overlap=["VIEW", "MANAGE"],
)
3. Missing Negative Test CaseYou added a positive test:
Recommendation: Add a negative test case:
4. Code Clarity & Structure
5. Summary of Action Items
Suggested Additional Test Code def test_no_grants_denies_access(self):
"""Ensure access is denied when neither user nor group has permission"""
self.grants.exists.return_value = False
self.group_grants.exists.return_value = False
with self.assertRaises(AppUnauthorizedFailed):
_auth_system(self.file, "user")
def test_user_group_permissions_are_checked_if_user_permissions_fail(self):
"""Ensure group check happens even if user-specific check returns False"""
self.grants.exists.return_value = False
self.group_grants.exists.return_value = True # Group has permission
# Should NOT raise exception
_auth_system(self.file, "user")
# Verify group grants were checked
self.assertTrue(self.group_grants.filter.called) |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
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.
Here is an analysis of the code changes provided, focusing on irregularities, potential issues, and optimization suggestions.
1. Irregularities & Potential Issues
A. Logic Inconsistency with Default
workspace_idworkspace_idhandles defaults.is_valid:workspace_id = self.data.get("workspace_id", "None"). It retrieves the value from the request data. If missing, it sets it to the string"None".insert: The user passes aworkspace_idargument. This is passed tois_valid.QuerySet.filter: It uses thisworkspace_id.self.datacontainsworkspace_idbut the caller passes a different workspace_id (or vice versa), or ifself.datalacks it while the caller provides it, the logic might behave unexpectedly depending on which path is taken. Specifically, overriding the method signature to acceptworkspace_idbut then ignoring it ifself.datahas one (via.get(...)) creates ambiguous precedence.is_validshould validate strictly against input data. Business logic dependencies likeworkspace_idscoping should ideally be handled either entirely via serializer context or explicitly passed down without mixing sources (self.datavs. function arguments) for the same concept unless carefully controlled.B. String
"None"vs. PythonNoneworkspace_idis set to the string"None". Later,workspace_id=workspace_idis used in the DB filter. If a valid workspace ID ever legitimately equals the string"None", this will cause collision bugs.None(Python NULL) for unprovided values if the database column allows NULLs, or ensure strictly that"None"is reserved and never used as a real ID. If using strings, consider explicit sentinel values (e.g.,"default") rather than capitalized"None".C. Error Handling Scope
try...exceptblock only catchesAppApiExceptionwith specific codeValidCode.model_not_fount. Ifprovider_handlersetup raises another exception or if other validation errors raise unexpected exceptions, they may propagate incorrectly or result in status not being updated appropriately.2. Optimization Suggestions
A. Avoid
QuerySet.filter().exists()for Simple Checks (Minor Optimization)While
.exists()is generally efficient, you are filtering byname + workspace_id. Ensure there is a proper composite index on(name, workspace_id)in the Model to prevent full table scans, especially if this check happens during frequent inserts.B. Extract Validation Logic
The
is_validmethod is growing large. Consider extracting individual checks into separate methods for readability and testability:validate_provider()validate_fields_structure()check_unique_constraint()C. Context Instead of Parameter Overriding
In Django REST Framework, passing extra parameters to
is_validlikeworkspace_id=Noneis uncommon and breaks standard serializer interfaces. Consider one of these cleaner approaches:context['workspace_id']when creating the serializer instance, then access it inis_validviaself.context.get('workspace_id').is_validpurely for input format validation. Move the database uniqueness check and business rule validation to theinsertmethod or a dedicated service class, whereworkspace_idis naturally available.Example using Context:
D. Naming Convention
Avoid modifying external library method signatures (
is_valid). It violates the Liskov Substitution Principle and makes debugging harder. If you must override, stick to the original signature.Summary of Recommended Actions:
self.contextfor passingworkspace_idinto the serializer instead of altering theis_validsignature.(workspace_id, name)to optimize the duplicate check.None(Python null) or a reserved string like'global'/'default'for unset workspaces, and document it.