feat: add super admin bypass in tree list filter

Super admins now see all trees regardless of ownership, team, or
public/default status. Previously the build_tree_access_filter function
had no super_admin check, so admins could only see their own trees plus
public/default/team trees.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-05 23:25:56 -05:00
parent 71ba0b95a5
commit 02d06acfb8
2 changed files with 40 additions and 4 deletions

View File

@@ -2,7 +2,7 @@ from typing import Annotated, Optional
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, or_ from sqlalchemy import select, func, or_, true as sa_true
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from app.core.database import get_db from app.core.database import get_db
@@ -22,17 +22,22 @@ def build_tree_access_filter(current_user: User):
"""Build the access filter for trees based on user permissions. """Build the access filter for trees based on user permissions.
Returns trees that are: Returns trees that are:
- All trees (for super admins)
- Default/system trees (visible to all) - Default/system trees (visible to all)
- Public trees - Public trees
- User's own trees - User's own trees
- Trees from user's team - Trees from user's team
""" """
return or_( if current_user.is_super_admin:
return sa_true()
conditions = [
Tree.is_default == True, Tree.is_default == True,
Tree.is_public == True, Tree.is_public == True,
Tree.author_id == current_user.id, Tree.author_id == current_user.id,
Tree.team_id == current_user.team_id if current_user.team_id else False ]
) if current_user.team_id:
conditions.append(Tree.team_id == current_user.team_id)
return or_(*conditions)
def build_tree_response(tree: Tree) -> TreeListResponse: def build_tree_response(tree: Tree) -> TreeListResponse:

View File

@@ -172,6 +172,37 @@ class TestTrees:
active_ids = [tree["id"] for tree in active_trees] active_ids = [tree["id"] for tree in active_trees]
assert test_tree["id"] not in active_ids assert test_tree["id"] not in active_ids
@pytest.mark.asyncio
async def test_super_admin_sees_all_trees(
self, client: AsyncClient, auth_headers: dict, admin_auth_headers: dict
):
"""Test that super admin can see all trees including private ones from other users."""
# Create a private (non-public, non-default) tree as a regular user
tree_data = {
"name": "Private User Tree",
"description": "Only visible to author and super admin",
"tree_structure": {
"id": "root",
"type": "solution",
"title": "Private",
"description": "Private tree"
},
"is_public": False,
"is_default": False
}
create_response = await client.post(
"/api/v1/trees", json=tree_data, headers=auth_headers
)
assert create_response.status_code == 201
private_tree_id = create_response.json()["id"]
# Super admin should see it in list
list_response = await client.get("/api/v1/trees", headers=admin_auth_headers)
assert list_response.status_code == 200
tree_ids = [t["id"] for t in list_response.json()]
assert private_tree_id in tree_ids
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_tree_unauthorized(self, client: AsyncClient): async def test_create_tree_unauthorized(self, client: AsyncClient):
"""Test that creating a tree without auth fails.""" """Test that creating a tree without auth fails."""