Repository navigation
fix: enhance folder permission checks by incorporating user group permissions - #7884
Conversation
| root = next(node for node in result if node["id"] == self.workspace_id) | ||
| child_ids = [child["id"] for child in root["children"]] | ||
| self.assertIn(self.allowed_folder_id, child_ids) | ||
| self.assertNotIn(self.other_folder_id, child_ids) |
There was a problem hiding this comment.
The test is focused and readable, but there are several robustness issues and optimization opportunities.
Key concerns
-
Same folder IDs reused for all three folder types
allowed_folder_id,other_folder_id, andworkspace_idare reused as primary keys inKnowledgeFolder,ApplicationFolder, andToolFolder.- This is usually fine only if these are completely separate tables. If these models share a base table, use multi-table inheritance, or are involved in generic relations, this can cause collisions or mask bugs.
- More importantly, because the permission is created for every
auth_target_typewith the same target ID, a bug where the serializer ignoresauth_target_typewould still pass. The test should verify source/type isolation with distinct folder IDs per source.
-
Serializer method called without validation
result = FolderTreeSerializer( data={"workspace_id": self.workspace_id, "source": source} ).get_folder_tree(...)
The serializer is never validated with
.is_valid(). Ifget_folder_tree()depends onvalidated_data, this test may fail or behave differently from production. Prefer:serializer = FolderTreeSerializer(data={...}) serializer.is_valid(raise_exception=True) result = serializer.get_folder_tree(self.user, name=None)
If the method is intentionally called directly, add a comment explaining that it does not depend on
validated_data. -
Using
SimpleNamespace(id=self.user_id)instead of the real user- Pass
self.userinstead. This is more realistic and avoids hiding cases where the serializer uses other user attributes.
- Pass
-
Raw password and hard-coded role
password="x", role="USER",
Prefer
User.objects.create_user(...)or callset_password(). Also verify that"USER"is a valid role value or use the model’s choice constant. -
Possible missing workspace fixture
- The test assumes
workspace_idis just a UUID field. If any of these models use a foreign key to aWorkspace, the test may fail or be invalid because noWorkspaceobject is created.
- The test assumes
-
Assertions are a bit shallow
assertNotIn(self.other_folder_id, child_ids)only checks direct children of the root. It would not catchother_folder_idappearing deeper in the tree.assertIn(...)does not catch duplicate nodes. Consider:
self.assertEqual(child_ids.count(self.allowed_folder_id), 1)
and a recursive check that
other_folder_idis absent from the entire tree. -
Root ID equals workspace ID
root = folder_model.objects.create(id=self.workspace_id, ...)
This may be an implementation detail. If the root folder ID is not guaranteed to equal
workspace_id, use a separateroot_idand assert against that.
Optimization suggestions
- Use
setUpTestDatainstead ofsetUpif the suite grows. It runs once per test class and is faster. - Use unique IDs per folder type, or create the permission only inside each
subTestfor the currentsource. - Assert that
is_workspace_managewas called if that behavior matters. - Consider extracting a helper to create a root/allowed/other folder set to reduce duplication.
Example improvement sketch
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
id=cls.user_id,
username="folder-tree-user",
nick_name="folder tree user",
password="x",
role="USER",
)
cls.group = SystemUserGroup.objects.create(
id=cls.group_id, name="group B", workspace_id=cls.workspace_id
)
SystemUserGroupRelation.objects.create(user=cls.user, group=cls.group)
cls.folder_ids = {}
for source, folder_model in cls.folder_types:
root_id = uuid.uuid4()
allowed_id = uuid.uuid4()
other_id = uuid.uuid4()
root = folder_model.objects.create(
id=root_id, name="root", workspace_id=cls.workspace_id
)
folder_model.objects.create(
id=allowed_id, name="111", workspace_id=cls.workspace_id, parent=root
)
folder_model.objects.create(
id=other_id, name="other", workspace_id=cls.workspace_id, parent=root
)
WorkspaceUserGroupResourcePermission.objects.create(
workspace_id=cls.workspace_id,
user_group=cls.group,
auth_target_type=source,
target=allowed_id,
permission_list=["VIEW"],
)
cls.folder_ids[source] = {
"root_id": root_id,
"allowed_id": allowed_id,
"other_id": other_id,
}And in the test, use self.user and assert recursively that the disallowed folder is absent.
Overall: the test covers the happy path, but it could be made more reliable by avoiding shared IDs across folder types, using the real user object, validating the serializer if appropriate, and strengthening the assertions.
| ) | ||
|
|
||
| nodes = Folder.objects.filter(base_q).get_cached_trees() | ||
|
|
There was a problem hiding this comment.
Here is an analysis of the code changes, highlighting potential issues, logic checks, and optimization suggestions.
1. Logic & Correctness
- Correct Permission Union: The change correctly implements a union of permissions. A user now has access if they have direct
WorkspaceUserResourcePermissionOR viaGroupMembership(user_group__user_relations). This is standard RBAC (Role-Based Access Control) logic. permission_conditionScope: Ensure that the variablepermission_condition(a list like["VIEW", "ROLE"]) is correctly defined inside thehaving_read_permission_by_roleblock and accessible when building the queries for both users and groups. If it's defined conditionally, ensure the subsequent lines are only executed when appropriate.values_listEfficiency: Using.values_list("target", flat=True)is efficient because it avoids instantiating model objects.
2. Potential Issues
- N+1 Query / Subquery Overhead: While not an N+1 issue in the traditional loop sense, chaining these subqueries can lead to complex SQL plans. However, Django ORM usually generates a single query with
ORconditions and subqueries, which is acceptable. - Indexing: For performance, ensure that database indexes exist on:
WorkspaceUserResourcePermission(user_id, auth_target_type, workspace_id)WorkspaceUserGroupResourcePermission(user_group, auth_target_type, workspace_id)- Any intermediate join tables for
user_group__user_relations.
- Empty Querysets: If
current_userhas no direct or group permissions, theQ(id__in=...)clauses will filter nothing, but the| Q(id=self.data.get("workspace_id"))ensures at least the workspace root is included (assuming that’s intended behavior). Verify if users with no explicit permissions should indeed see the workspace root or not. Often, workspace visibility is controlled separately.
3. Optimization & Code Quality Suggestions
- Variable Naming: Clear and descriptive. Good.
- DRY Principle: There is some repetition in filtering by
auth_target_typeandworkspace_id. You could refactor slightly for readability, though the current version is clear enough. - Query Combination: Instead of two separate
values_listcalls followed by combining Q objects, you could potentially combine the queries before evaluating. However, the current approach is fine. Here is a slightly more optimized pattern using a single combined query if the models support it directly, but since they are different models, separate queries are necessary.
Suggested Refactor (Minor Readability Improvement)
Current code is fine, but wrapping the permission filters into helper methods or variables if used elsewhere would be better. For this snippet, it's okay as-is.
4. Final Verdict
✅ No critical bugs detected.
WorkspaceUserGroupResourcePermission lookups, especially traversing relationships (user_group__user_relations__user_id).
💡 Logic Check: Confirm business logic allows default visibility to workspace_id even without explicit folder permissions.
Optimized Version (Optional)
If you want to slightly tighten the syntax:
# Inside get_folder_tree after defining permission_condition
direct_perms = WorkspaceUserResourcePermission.objects.filter(
user_id=current_user.id,
auth_target_type=self.data.get("source"),
workspace_id=self.data.get("workspace_id"),
permission_list__overlap=permission_condition,
).values_list("target", flat=True)
group_perms = WorkspaceUserGroupResourcePermission.objects.filter(
user_group__user_relations__user_id=current_user.id,
auth_target_type=self.data.get("source"),
workspace_id=self.data.get("workspace_id"),
permission_list__overlap=permission_condition,
).values_list("target", flat=True)
base_q &= (
Q(id__in=direct_perms) |
Q(id__in=group_perms) |
Q(id=self.data.get("workspace_id"))
)This matches your current logic exactly. No functional change needed unless indexing is missing.
fix: enhance folder permission checks by incorporating user group permissions