Skip to content

fix: enhance folder permission checks by incorporating user group permissions - #7884

Merged
wxg0103 merged 1 commit into
v3from
pr@v3@fix_folder_permission
Nov 13, 2026
Merged

wxg0103 merged 1 commit into
v3from
pr@v3@fix_folder_permission

Conversation

@shaohuzhang1

Copy link
Copy Markdown
Contributor

fix: enhance folder permission checks by incorporating user group permissions

@wxg0103
wxg0103 merged commit a8467c7 into v3 Nov 13, 2026
3 of 4 checks passed
@wxg0103
wxg0103 deleted the pr@v3@fix_folder_permission branch November 13, 2026 02:59
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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The test is focused and readable, but there are several robustness issues and optimization opportunities.

Key concerns

  1. Same folder IDs reused for all three folder types

    • allowed_folder_id, other_folder_id, and workspace_id are reused as primary keys in KnowledgeFolder, ApplicationFolder, and ToolFolder.
    • 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_type with the same target ID, a bug where the serializer ignores auth_target_type would still pass. The test should verify source/type isolation with distinct folder IDs per source.
  2. 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(). If get_folder_tree() depends on validated_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.

  3. Using SimpleNamespace(id=self.user_id) instead of the real user

    • Pass self.user instead. This is more realistic and avoids hiding cases where the serializer uses other user attributes.
  4. Raw password and hard-coded role

    password="x",
    role="USER",

    Prefer User.objects.create_user(...) or call set_password(). Also verify that "USER" is a valid role value or use the model’s choice constant.

  5. Possible missing workspace fixture

    • The test assumes workspace_id is just a UUID field. If any of these models use a foreign key to a Workspace, the test may fail or be invalid because no Workspace object is created.
  6. Assertions are a bit shallow

    • assertNotIn(self.other_folder_id, child_ids) only checks direct children of the root. It would not catch other_folder_id appearing 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_id is absent from the entire tree.

  7. 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 separate root_id and assert against that.

Optimization suggestions

  • Use setUpTestData instead of setUp if 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 subTest for the current source.
  • Assert that is_workspace_manage was 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()

Copy link
Copy Markdown
Contributor Author

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, 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 WorkspaceUserResourcePermission OR via GroupMembership (user_group__user_relations). This is standard RBAC (Role-Based Access Control) logic.
  • permission_condition Scope: Ensure that the variable permission_condition (a list like ["VIEW", "ROLE"]) is correctly defined inside the having_read_permission_by_role block 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_list Efficiency: 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 OR conditions 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_user has no direct or group permissions, the Q(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_type and workspace_id. You could refactor slightly for readability, though the current version is clear enough.
  • Query Combination: Instead of two separate values_list calls 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.
⚠️ Check Indexes: Ensure DB indexes to support the new 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants