diff --git a/backend/open_webui/routers/groups.py b/backend/open_webui/routers/groups.py
index ff3a1997f7..3a06cb287c 100755
--- a/backend/open_webui/routers/groups.py
+++ b/backend/open_webui/routers/groups.py
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES
from open_webui.internal.db import get_async_session
+from open_webui.models.access_grants import AccessGrants
from open_webui.models.groups import (
GroupForm,
GroupInfoResponse,
@@ -15,6 +16,9 @@ from open_webui.models.groups import (
GroupUpdateForm,
UserIdsForm,
)
+from open_webui.models.knowledge import Knowledges
+from open_webui.models.models import Models
+from open_webui.models.tools import Tools
from open_webui.models.users import UserInfoResponse, Users
from open_webui.utils.auth import get_admin_user, get_verified_user
from sqlalchemy.ext.asyncio import AsyncSession
@@ -273,3 +277,88 @@ async def delete_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSes
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
+
+
+############################
+# PreviewGroupAccess
+############################
+
+
+@router.get('/id/{id}/preview')
+async def preview_group_access(
+ id: str,
+ user=Depends(get_admin_user),
+ db: AsyncSession = Depends(get_async_session),
+):
+ """Show what resources a group can access (preview audit)."""
+ group = await Groups.get_group_by_id(id, db=db)
+ if not group:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=ERROR_MESSAGES.NOT_FOUND,
+ )
+
+ group_ids = {group.id}
+
+ # Batch-check accessible resources using existing AccessGrants
+ all_models = await Models.get_all_models(db=db)
+ accessible_model_ids = await AccessGrants.get_accessible_resource_ids(
+ user_id='',
+ resource_type='model',
+ resource_ids=[m.id for m in all_models],
+ permission='read',
+ user_group_ids=group_ids,
+ db=db,
+ )
+
+ all_knowledge = await Knowledges.get_knowledge_bases(db=db)
+ accessible_knowledge_ids = await AccessGrants.get_accessible_resource_ids(
+ user_id='',
+ resource_type='knowledge',
+ resource_ids=[k.id for k in all_knowledge],
+ permission='read',
+ user_group_ids=group_ids,
+ db=db,
+ )
+
+ all_tools = await Tools.get_tools(defer_content=True, db=db)
+ accessible_tool_ids = await AccessGrants.get_accessible_resource_ids(
+ user_id='',
+ resource_type='tool',
+ resource_ids=[t.id for t in all_tools],
+ permission='read',
+ user_group_ids=group_ids,
+ db=db,
+ )
+
+ active_models = [m for m in all_models if m.is_active]
+
+ return {
+ 'group': {'id': group.id, 'name': group.name},
+ 'models': {
+ 'items': [
+ {'id': m.id, 'name': m.name}
+ for m in active_models
+ if m.id in accessible_model_ids
+ ],
+ 'total': len(active_models),
+ },
+ 'knowledge': {
+ 'items': [
+ {'id': k.id, 'name': k.name}
+ for k in all_knowledge
+ if k.id in accessible_knowledge_ids
+ ],
+ 'total': len(all_knowledge),
+ },
+ 'tools': {
+ 'items': [
+ {'id': t.id, 'name': t.name}
+ for t in all_tools
+ if t.id in accessible_tool_ids
+ ],
+ 'total': len(all_tools),
+ },
+ 'permissions': group.permissions or {},
+ }
+
diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py
index c4a54744c5..3c215c8f16 100644
--- a/backend/open_webui/routers/users.py
+++ b/backend/open_webui/routers/users.py
@@ -3,6 +3,7 @@ from __future__ import annotations
import base64
import io
import logging
+import time
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status
@@ -25,6 +26,10 @@ from open_webui.models.users import (
UserStatus,
UserUpdateForm,
)
+from open_webui.models.access_grants import AccessGrants
+from open_webui.models.knowledge import Knowledges
+from open_webui.models.models import Models
+from open_webui.models.tools import Tools
from open_webui.socket.main import disconnect_user_sessions
from open_webui.utils.access_control import get_permissions, has_permission
from open_webui.utils.auth import (
@@ -677,3 +682,88 @@ async def get_user_groups_by_id(
user_id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)
):
return await Groups.get_groups_by_member_id(user_id, db=db)
+
+
+############################
+# GetUserPreview
+############################
+
+
+@router.get('/{user_id}/preview')
+async def get_user_preview(
+ user_id: str,
+ user=Depends(get_admin_user),
+ db: AsyncSession = Depends(get_async_session),
+):
+ """Show what resources a specific user can access across all their groups."""
+ target_user = await Users.get_user_by_id(user_id, db=db)
+ if not target_user:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=ERROR_MESSAGES.USER_NOT_FOUND,
+ )
+
+ # Get all group IDs this user belongs to
+ user_groups = await Groups.get_groups_by_member_id(user_id, db=db)
+ user_group_ids = {g.id for g in user_groups}
+
+ all_models = await Models.get_all_models(db=db)
+ accessible_model_ids = await AccessGrants.get_accessible_resource_ids(
+ user_id=user_id,
+ resource_type='model',
+ resource_ids=[m.id for m in all_models],
+ permission='read',
+ user_group_ids=user_group_ids,
+ db=db,
+ )
+
+ all_knowledge = await Knowledges.get_knowledge_bases(db=db)
+ accessible_knowledge_ids = await AccessGrants.get_accessible_resource_ids(
+ user_id=user_id,
+ resource_type='knowledge',
+ resource_ids=[k.id for k in all_knowledge],
+ permission='read',
+ user_group_ids=user_group_ids,
+ db=db,
+ )
+
+ all_tools = await Tools.get_tools(defer_content=True, db=db)
+ accessible_tool_ids = await AccessGrants.get_accessible_resource_ids(
+ user_id=user_id,
+ resource_type='tool',
+ resource_ids=[t.id for t in all_tools],
+ permission='read',
+ user_group_ids=user_group_ids,
+ db=db,
+ )
+
+ active_models = [m for m in all_models if m.is_active]
+
+ return {
+ 'user': {'id': target_user.id, 'name': target_user.name},
+ 'groups': [{'id': g.id, 'name': g.name} for g in user_groups],
+ 'models': {
+ 'items': [
+ {'id': m.id, 'name': m.name}
+ for m in active_models
+ if m.id in accessible_model_ids
+ ],
+ 'total': len(active_models),
+ },
+ 'knowledge': {
+ 'items': [
+ {'id': k.id, 'name': k.name}
+ for k in all_knowledge
+ if k.id in accessible_knowledge_ids
+ ],
+ 'total': len(all_knowledge),
+ },
+ 'tools': {
+ 'items': [
+ {'id': t.id, 'name': t.name}
+ for t in all_tools
+ if t.id in accessible_tool_ids
+ ],
+ 'total': len(all_tools),
+ },
+ }
diff --git a/src/lib/apis/groups/index.ts b/src/lib/apis/groups/index.ts
index 6089a6023f..dfc6aa6a71 100644
--- a/src/lib/apis/groups/index.ts
+++ b/src/lib/apis/groups/index.ts
@@ -267,3 +267,31 @@ export const removeUserFromGroup = async (token: string, id: string, userIds: st
return res;
};
+
+export const getGroupPreview = async (token: string, id: string) => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/preview`, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .catch((err) => {
+ error = err.detail;
+ console.error(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts
index 91b63338de..267aa69d22 100644
--- a/src/lib/apis/users/index.ts
+++ b/src/lib/apis/users/index.ts
@@ -550,3 +550,30 @@ export const getUserGroupsById = async (token: string, userId: string) => {
return res;
};
+
+export const getUserPreview = async (token: string, userId: string) => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/preview`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .catch((err) => {
+ console.error(err);
+ error = err.detail;
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
diff --git a/src/lib/components/admin/UserPreviewModal.svelte b/src/lib/components/admin/UserPreviewModal.svelte
new file mode 100644
index 0000000000..ce2c66bde5
--- /dev/null
+++ b/src/lib/components/admin/UserPreviewModal.svelte
@@ -0,0 +1,175 @@
+
+
+
+ {/if}
+
+
+
+
+
+