feat: add opt-in scope_user_search_to_org flag for /user/filter/ui
PR #22722 made org-scoping unconditional on /user/filter/ui, which broke team admins who aren't org admins (403 when searching users to add). This makes org-scoping opt-in via a new UI Settings toggle, restoring open search by default. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cfd0e2cf99
commit
c631708df6
@ -1836,6 +1836,10 @@ async def ui_view_users(
|
||||
user_email: Optional[str] = fastapi.Query(
|
||||
default=None, description="User email in the request parameters"
|
||||
),
|
||||
team_id: Optional[str] = fastapi.Query(
|
||||
default=None,
|
||||
description="Team ID — used when a team admin searches for users to add to their team",
|
||||
),
|
||||
page: int = fastapi.Query(
|
||||
default=1, description="Page number for pagination", ge=1
|
||||
),
|
||||
@ -1847,20 +1851,19 @@ async def ui_view_users(
|
||||
"""
|
||||
Filter users based on partial match of user_id or email with pagination.
|
||||
|
||||
- Proxy admins: receive all matching users.
|
||||
- Organization admins: receive only users in their own organization(s).
|
||||
- Other roles: access denied (403).
|
||||
Behaviour depends on the ``scope_user_search_to_org`` UI-setting flag
|
||||
(stored in the ``litellm_uisettings`` table):
|
||||
|
||||
Args:
|
||||
user_id (Optional[str]): Partial user ID to search for
|
||||
user_email (Optional[str]): Partial email to search for
|
||||
page (int): Page number for pagination (starts at 1)
|
||||
page_size (int): Number of items per page (max 100)
|
||||
user_api_key_dict (UserAPIKeyAuth): User authentication information
|
||||
|
||||
Returns:
|
||||
List of matching user records (LiteLLM_UserTableFiltered), scoped by org for org admins.
|
||||
* **Flag OFF (default):** any authenticated user can search all users.
|
||||
* **Flag ON:**
|
||||
- Proxy admins see all users.
|
||||
- Org admins see only users in their org(s).
|
||||
- Team admins for an org-bound team see users in that org.
|
||||
- Others receive a 403.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.common_utils import (
|
||||
_is_user_team_admin,
|
||||
)
|
||||
from litellm.proxy.proxy_server import (
|
||||
prisma_client,
|
||||
proxy_logging_obj,
|
||||
@ -1871,51 +1874,84 @@ async def ui_view_users(
|
||||
raise HTTPException(status_code=500, detail={"error": "No db connected"})
|
||||
|
||||
try:
|
||||
# Restrict by caller role: proxy admin sees all; org admin sees only their org(s); others 403
|
||||
is_proxy_admin = _user_has_admin_view(user_api_key_dict)
|
||||
if not is_proxy_admin:
|
||||
if user_api_key_dict.user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only proxy admins and organization admins can search users."
|
||||
},
|
||||
)
|
||||
try:
|
||||
caller_user = await get_user_object(
|
||||
user_id=user_api_key_dict.user_id,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
user_id_upsert=False,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
except ValueError:
|
||||
# get_user_object raises ValueError when user not found (user_id_upsert=False)
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only proxy admins and organization admins can search users."
|
||||
},
|
||||
)
|
||||
if caller_user is None:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only proxy admins and organization admins can search users."
|
||||
},
|
||||
)
|
||||
org_admin_org_ids = [
|
||||
m.organization_id
|
||||
for m in (caller_user.organization_memberships or [])
|
||||
if m.user_role == LitellmUserRoles.ORG_ADMIN.value
|
||||
]
|
||||
if not org_admin_org_ids:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only proxy admins and organization admins can search users."
|
||||
},
|
||||
)
|
||||
# Read the scope_user_search_to_org flag from the DB
|
||||
ui_settings_row = (
|
||||
await prisma_client.db.litellm_uisettings.find_unique(
|
||||
where={"id": "ui_settings"}
|
||||
)
|
||||
)
|
||||
scope_flag = False
|
||||
if ui_settings_row is not None:
|
||||
settings_json = ui_settings_row.settings or {} # type: ignore[union-attr]
|
||||
scope_flag = bool(settings_json.get("scope_user_search_to_org", False))
|
||||
|
||||
org_filter_ids: Optional[List[str]] = None
|
||||
|
||||
if scope_flag:
|
||||
is_proxy_admin = _user_has_admin_view(user_api_key_dict)
|
||||
if not is_proxy_admin:
|
||||
# Try to resolve org admin memberships
|
||||
caller_user = None
|
||||
if user_api_key_dict.user_id is not None:
|
||||
try:
|
||||
caller_user = await get_user_object(
|
||||
user_id=user_api_key_dict.user_id,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
user_id_upsert=False,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
except ValueError:
|
||||
caller_user = None
|
||||
|
||||
org_admin_org_ids: List[str] = []
|
||||
if caller_user is not None:
|
||||
org_admin_org_ids = [
|
||||
m.organization_id
|
||||
for m in (caller_user.organization_memberships or [])
|
||||
if m.user_role == LitellmUserRoles.ORG_ADMIN.value
|
||||
]
|
||||
|
||||
if org_admin_org_ids:
|
||||
org_filter_ids = org_admin_org_ids
|
||||
elif team_id is not None:
|
||||
# Look up the team to check if it belongs to an org
|
||||
team_row = await prisma_client.db.litellm_teamtable.find_unique(
|
||||
where={"team_id": team_id}
|
||||
)
|
||||
if team_row is not None:
|
||||
team_obj = LiteLLM_TeamTable(**team_row.model_dump())
|
||||
if _is_user_team_admin(user_api_key_dict, team_obj):
|
||||
if team_obj.organization_id:
|
||||
org_filter_ids = [team_obj.organization_id]
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "scope_user_search_to_org is enabled and this team is not part of an organization. Contact your proxy admin to adjust this setting."
|
||||
},
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "scope_user_search_to_org is enabled. Only proxy admins, organization admins, or team admins can search users."
|
||||
},
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "scope_user_search_to_org is enabled. Only proxy admins, organization admins, or team admins can search users."
|
||||
},
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "scope_user_search_to_org is enabled. Only proxy admins, organization admins, or team admins can search users."
|
||||
},
|
||||
)
|
||||
|
||||
# Calculate offset for pagination
|
||||
skip = (page - 1) * page_size
|
||||
@ -1935,10 +1971,10 @@ async def ui_view_users(
|
||||
"mode": "insensitive", # Case-insensitive search
|
||||
}
|
||||
|
||||
# Org admins: only users in their org(s)
|
||||
if not is_proxy_admin:
|
||||
# Apply org filter when scope_user_search_to_org is ON and caller is not proxy admin
|
||||
if org_filter_ids is not None:
|
||||
where_conditions["organization_memberships"] = {
|
||||
"some": {"organization_id": {"in": org_admin_org_ids}}
|
||||
"some": {"organization_id": {"in": org_filter_ids}}
|
||||
}
|
||||
|
||||
# Query users with pagination and filters
|
||||
|
||||
@ -124,6 +124,11 @@ class UISettings(BaseModel):
|
||||
description="If true, team admins are exempt from the vector stores disable restriction (only takes effect when disable_vector_stores_for_internal_users is true).",
|
||||
)
|
||||
|
||||
scope_user_search_to_org: bool = Field(
|
||||
default=False,
|
||||
description="If enabled, the user search endpoint (/user/filter/ui) restricts results by organization. When off, any authenticated user can search all users.",
|
||||
)
|
||||
|
||||
|
||||
class UISettingsResponse(SettingsResponse):
|
||||
"""Response model for UI settings"""
|
||||
@ -143,6 +148,7 @@ ALLOWED_UI_SETTINGS_FIELDS = {
|
||||
"allow_agents_for_team_admins",
|
||||
"disable_vector_stores_for_internal_users",
|
||||
"allow_vector_stores_for_team_admins",
|
||||
"scope_user_search_to_org",
|
||||
}
|
||||
|
||||
# Flags that must be synced from the persisted UISettings into
|
||||
|
||||
@ -54,6 +54,12 @@ async def test_ui_view_users_with_null_email(mocker, caplog):
|
||||
|
||||
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
|
||||
|
||||
# Flag OFF by default — no settings row
|
||||
async def mock_find_unique_settings(*args, **kwargs):
|
||||
return None
|
||||
|
||||
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
|
||||
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
|
||||
# Proxy admin: no org filter, no get_user_object call
|
||||
@ -63,6 +69,7 @@ async def test_ui_view_users_with_null_email(mocker, caplog):
|
||||
),
|
||||
user_id="test_user",
|
||||
user_email=None,
|
||||
team_id=None,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
@ -83,6 +90,12 @@ async def test_ui_view_users_proxy_admin_no_org_filter(mocker):
|
||||
return []
|
||||
|
||||
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
|
||||
|
||||
# Flag OFF by default
|
||||
async def mock_find_unique_settings(*args, **kwargs):
|
||||
return None
|
||||
|
||||
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
|
||||
await ui_view_users(
|
||||
@ -91,6 +104,7 @@ async def test_ui_view_users_proxy_admin_no_org_filter(mocker):
|
||||
),
|
||||
user_id=None,
|
||||
user_email="foo",
|
||||
team_id=None,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
@ -99,8 +113,8 @@ async def test_ui_view_users_proxy_admin_no_org_filter(mocker):
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_users_org_admin_filtered_by_org(mocker):
|
||||
"""
|
||||
Org admin: find_many is called with organization_memberships filter so only users
|
||||
in the caller's org(s) are returned.
|
||||
Org admin with scope_user_search_to_org ON: find_many is called with
|
||||
organization_memberships filter so only users in the caller's org(s) are returned.
|
||||
"""
|
||||
from litellm.proxy._types import LiteLLM_OrganizationMembershipTable
|
||||
|
||||
@ -116,6 +130,16 @@ async def test_ui_view_users_org_admin_filtered_by_org(mocker):
|
||||
return []
|
||||
|
||||
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
|
||||
|
||||
# Flag ON
|
||||
mock_settings_row = mocker.MagicMock()
|
||||
mock_settings_row.settings = {"scope_user_search_to_org": True}
|
||||
|
||||
async def mock_find_unique_settings(*args, **kwargs):
|
||||
return mock_settings_row
|
||||
|
||||
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
|
||||
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
|
||||
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
|
||||
@ -143,6 +167,7 @@ async def test_ui_view_users_org_admin_filtered_by_org(mocker):
|
||||
user_api_key_dict=UserAPIKeyAuth(user_id="org-admin", user_role=None),
|
||||
user_id=None,
|
||||
user_email="u",
|
||||
team_id=None,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
@ -153,11 +178,21 @@ async def test_ui_view_users_org_admin_filtered_by_org(mocker):
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_users_non_org_admin_returns_403(mocker):
|
||||
"""
|
||||
Caller is not proxy admin and not org admin: endpoint returns 403.
|
||||
Flag ON, caller is not proxy admin and not org admin, no team_id: endpoint returns 403.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
|
||||
# Flag ON
|
||||
mock_settings_row = mocker.MagicMock()
|
||||
mock_settings_row.settings = {"scope_user_search_to_org": True}
|
||||
|
||||
async def mock_find_unique_settings(*args, **kwargs):
|
||||
return mock_settings_row
|
||||
|
||||
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
|
||||
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
|
||||
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
|
||||
@ -179,12 +214,237 @@ async def test_ui_view_users_non_org_admin_returns_403(mocker):
|
||||
user_api_key_dict=UserAPIKeyAuth(user_id="internal_user", user_role=None),
|
||||
user_id=None,
|
||||
user_email="u",
|
||||
team_id=None,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "Only proxy admins and organization admins" in str(exc_info.value.detail)
|
||||
assert "scope_user_search_to_org is enabled" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_users_flag_off_internal_user_can_search(mocker):
|
||||
"""
|
||||
Flag OFF (default): any authenticated user can search all users without org filtering.
|
||||
"""
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
|
||||
async def mock_find_many(*args, **kwargs):
|
||||
where = kwargs.get("where") or {}
|
||||
assert "organization_memberships" not in where
|
||||
return []
|
||||
|
||||
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
|
||||
|
||||
# Flag OFF — no settings row
|
||||
async def mock_find_unique_settings(*args, **kwargs):
|
||||
return None
|
||||
|
||||
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
|
||||
response = await ui_view_users(
|
||||
user_api_key_dict=UserAPIKeyAuth(user_id="internal_user", user_role=None),
|
||||
user_id=None,
|
||||
user_email="foo",
|
||||
team_id=None,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
|
||||
assert response == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_users_flag_on_team_admin_org_team(mocker):
|
||||
"""
|
||||
Flag ON, team admin for org-bound team: org filter is applied using team's org.
|
||||
"""
|
||||
from litellm.proxy._types import LiteLLM_TeamTable, Member
|
||||
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
org_id = "org-456"
|
||||
tid = "team-789"
|
||||
|
||||
async def mock_find_many(*args, **kwargs):
|
||||
where = kwargs.get("where") or {}
|
||||
assert "organization_memberships" in where
|
||||
assert where["organization_memberships"] == {
|
||||
"some": {"organization_id": {"in": [org_id]}}
|
||||
}
|
||||
return []
|
||||
|
||||
mock_prisma_client.db.litellm_usertable.find_many = mock_find_many
|
||||
|
||||
# Flag ON
|
||||
mock_settings_row = mocker.MagicMock()
|
||||
mock_settings_row.settings = {"scope_user_search_to_org": True}
|
||||
|
||||
async def mock_find_unique_settings(*args, **kwargs):
|
||||
return mock_settings_row
|
||||
|
||||
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
|
||||
|
||||
# Team lookup
|
||||
mock_team_row = mocker.MagicMock()
|
||||
mock_team_row.model_dump.return_value = {
|
||||
"team_id": tid,
|
||||
"team_alias": "test-team",
|
||||
"organization_id": org_id,
|
||||
"members_with_roles": [{"user_id": "team-admin-user", "role": "admin"}],
|
||||
"admins": [],
|
||||
"members": [],
|
||||
"blocked": False,
|
||||
}
|
||||
|
||||
async def mock_find_unique_team(*args, **kwargs):
|
||||
return mock_team_row
|
||||
|
||||
mock_prisma_client.db.litellm_teamtable.find_unique = mock_find_unique_team
|
||||
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
|
||||
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
|
||||
|
||||
# Caller is not org admin
|
||||
caller_user = mocker.MagicMock()
|
||||
caller_user.organization_memberships = []
|
||||
|
||||
async def mock_get_user_object(*args, **kwargs):
|
||||
return caller_user
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.get_user_object",
|
||||
side_effect=mock_get_user_object,
|
||||
)
|
||||
|
||||
response = await ui_view_users(
|
||||
user_api_key_dict=UserAPIKeyAuth(user_id="team-admin-user", user_role=None),
|
||||
user_id=None,
|
||||
user_email="u",
|
||||
team_id=tid,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
|
||||
assert response == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_users_flag_on_team_admin_non_org_team_403(mocker):
|
||||
"""
|
||||
Flag ON, team admin for non-org team: returns 403.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
tid = "team-no-org"
|
||||
|
||||
# Flag ON
|
||||
mock_settings_row = mocker.MagicMock()
|
||||
mock_settings_row.settings = {"scope_user_search_to_org": True}
|
||||
|
||||
async def mock_find_unique_settings(*args, **kwargs):
|
||||
return mock_settings_row
|
||||
|
||||
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
|
||||
|
||||
# Team lookup — no organization_id
|
||||
mock_team_row = mocker.MagicMock()
|
||||
mock_team_row.model_dump.return_value = {
|
||||
"team_id": tid,
|
||||
"team_alias": "no-org-team",
|
||||
"organization_id": None,
|
||||
"members_with_roles": [{"user_id": "team-admin-user", "role": "admin"}],
|
||||
"admins": [],
|
||||
"members": [],
|
||||
"blocked": False,
|
||||
}
|
||||
|
||||
async def mock_find_unique_team(*args, **kwargs):
|
||||
return mock_team_row
|
||||
|
||||
mock_prisma_client.db.litellm_teamtable.find_unique = mock_find_unique_team
|
||||
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
|
||||
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
|
||||
|
||||
# Caller is not org admin
|
||||
caller_user = mocker.MagicMock()
|
||||
caller_user.organization_memberships = []
|
||||
|
||||
async def mock_get_user_object(*args, **kwargs):
|
||||
return caller_user
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.get_user_object",
|
||||
side_effect=mock_get_user_object,
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await ui_view_users(
|
||||
user_api_key_dict=UserAPIKeyAuth(
|
||||
user_id="team-admin-user", user_role=None
|
||||
),
|
||||
user_id=None,
|
||||
user_email="u",
|
||||
team_id=tid,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "not part of an organization" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_view_users_flag_on_non_admin_no_team_id_403(mocker):
|
||||
"""
|
||||
Flag ON, non-admin caller without team_id: returns 403.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
|
||||
# Flag ON
|
||||
mock_settings_row = mocker.MagicMock()
|
||||
mock_settings_row.settings = {"scope_user_search_to_org": True}
|
||||
|
||||
async def mock_find_unique_settings(*args, **kwargs):
|
||||
return mock_settings_row
|
||||
|
||||
mock_prisma_client.db.litellm_uisettings.find_unique = mock_find_unique_settings
|
||||
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", mocker.MagicMock())
|
||||
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", mocker.MagicMock())
|
||||
|
||||
# Caller is not org admin
|
||||
caller_user = mocker.MagicMock()
|
||||
caller_user.organization_memberships = []
|
||||
|
||||
async def mock_get_user_object(*args, **kwargs):
|
||||
return caller_user
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.get_user_object",
|
||||
side_effect=mock_get_user_object,
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await ui_view_users(
|
||||
user_api_key_dict=UserAPIKeyAuth(user_id="internal_user", user_role=None),
|
||||
user_id=None,
|
||||
user_email="u",
|
||||
team_id=None,
|
||||
page=1,
|
||||
page_size=50,
|
||||
)
|
||||
|
||||
assert exc_info.value.status_code == 403
|
||||
assert "scope_user_search_to_org is enabled" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
def test_user_daily_activity_types():
|
||||
|
||||
@ -23,6 +23,7 @@ export default function UISettings() {
|
||||
const allowAgentsTeamAdminsProperty = schema?.properties?.allow_agents_for_team_admins;
|
||||
const disableVectorStoresProperty = schema?.properties?.disable_vector_stores_for_internal_users;
|
||||
const allowVectorStoresTeamAdminsProperty = schema?.properties?.allow_vector_stores_for_team_admins;
|
||||
const scopeUserSearchProperty = schema?.properties?.scope_user_search_to_org;
|
||||
const values = data?.values ?? {};
|
||||
const isDisabledForInternalUsers = Boolean(values.disable_model_add_for_internal_users);
|
||||
const isDisabledTeamAdminDeleteTeamUser = Boolean(values.disable_team_admin_delete_team_user);
|
||||
@ -167,6 +168,20 @@ export default function UISettings() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggleScopeUserSearch = (checked: boolean) => {
|
||||
updateSettings(
|
||||
{ scope_user_search_to_org: checked },
|
||||
{
|
||||
onSuccess: () => {
|
||||
NotificationManager.success("UI settings updated successfully");
|
||||
},
|
||||
onError: (error) => {
|
||||
NotificationManager.fromBackend(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="UI Settings">
|
||||
{isLoading ? (
|
||||
@ -347,6 +362,26 @@ export default function UISettings() {
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Scope user search to organization */}
|
||||
<Space align="start" size="middle">
|
||||
<Switch
|
||||
checked={Boolean(values.scope_user_search_to_org)}
|
||||
disabled={isUpdating}
|
||||
loading={isUpdating}
|
||||
onChange={handleToggleScopeUserSearch}
|
||||
aria-label={scopeUserSearchProperty?.description ?? "Scope user search to organization"}
|
||||
/>
|
||||
<Space direction="vertical" size={4}>
|
||||
<Typography.Text strong>Scope user search to organization</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{scopeUserSearchProperty?.description ??
|
||||
"If enabled, the user search endpoint restricts results by organization. When off, any authenticated user can search all users."}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Page Visibility for Internal Users */}
|
||||
<PageVisibilitySettings
|
||||
enabledPagesInternalUsers={values.enabled_ui_pages_internal_users}
|
||||
|
||||
@ -35,6 +35,7 @@ interface UserSearchModalProps {
|
||||
title?: string;
|
||||
roles?: Role[];
|
||||
defaultRole?: string;
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
||||
@ -52,6 +53,7 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
||||
{ label: "user", value: "user", description: "User role. Can view team info, but not manage it." },
|
||||
],
|
||||
defaultRole = "user",
|
||||
teamId,
|
||||
}) => {
|
||||
const [form] = Form.useForm<FormValues>();
|
||||
const [userOptions, setUserOptions] = useState<UserOption[]>([]);
|
||||
@ -69,6 +71,9 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append(fieldName, searchText);
|
||||
if (teamId) {
|
||||
params.append("team_id", teamId);
|
||||
}
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2473,14 +2473,19 @@ export const allEndUsersCall = async (accessToken: string) => {
|
||||
|
||||
export const userFilterUICall = async (accessToken: string, params: URLSearchParams) => {
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/user/filter/ui` : `/user/filter/ui`;
|
||||
|
||||
const base = proxyBaseUrl ? `${proxyBaseUrl}/user/filter/ui` : `/user/filter/ui`;
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.get("user_email")) {
|
||||
url += `?user_email=${params.get("user_email")}`;
|
||||
queryParams.append("user_email", params.get("user_email")!);
|
||||
}
|
||||
if (params.get("user_id")) {
|
||||
url += `?user_id=${params.get("user_id")}`;
|
||||
queryParams.append("user_id", params.get("user_id")!);
|
||||
}
|
||||
if (params.get("team_id")) {
|
||||
queryParams.append("team_id", params.get("team_id")!);
|
||||
}
|
||||
const qs = queryParams.toString();
|
||||
const url = qs ? `${base}?${qs}` : base;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
|
||||
@ -1303,6 +1303,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
||||
onCancel={() => setIsAddMemberModalVisible(false)}
|
||||
onSubmit={handleMemberCreate}
|
||||
accessToken={accessToken}
|
||||
teamId={teamId}
|
||||
/>
|
||||
|
||||
{/* Delete Member Confirmation Modal */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user