Skip to content

Commit 6b0f702

Browse files
authored
♻️Refactoring authentication&authorization to develop the user management feature
♻️Refactoring authentication&authorization to develop the user management feature
2 parents d2a91a2 + c5a2b04 commit 6b0f702

71 files changed

Lines changed: 3678 additions & 2152 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/frontend/page_layer_rules.mdc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ description: Page layer rules for Next.js App Router pages and layouts
2121

2222
- Client components: `const { t } = useTranslation('namespace')`
2323
- Server components: `getTranslations` from `next-intl`
24+
25+
26+
2427
- Organize translation keys by feature/namespace
2528

2629
### Data Fetching

backend/apps/model_managment_app.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
BatchCreateModelsRequest,
1919
ModelRequest,
2020
ProviderModelRequest,
21+
AdminTenantModelRequest,
22+
AdminTenantModelResponse,
2123
)
2224

2325
from fastapi import APIRouter, Header, Query, HTTPException
@@ -39,6 +41,7 @@
3941
delete_model_for_tenant,
4042
list_models_for_tenant,
4143
list_llm_models_for_tenant,
44+
list_models_for_admin,
4245
)
4346
from utils.auth_utils import get_current_user_id
4447

@@ -285,6 +288,41 @@ async def get_llm_model_list(authorization: Optional[str] = Header(None)):
285288
detail=str(e))
286289

287290

291+
@router.post("/admin/list", response_model=AdminTenantModelResponse)
292+
async def get_model_list_for_admin(request: AdminTenantModelRequest, authorization: Optional[str] = Header(None)):
293+
"""Get model list for a specified tenant (admin only).
294+
295+
This endpoint allows super admin to query models for any tenant.
296+
297+
Args:
298+
request: Contains target tenant_id, optional model_type filter, and pagination params
299+
authorization: Bearer token for authentication
300+
301+
Returns:
302+
JSONResponse: Model list for the specified tenant with pagination info
303+
"""
304+
try:
305+
user_id, _ = get_current_user_id(authorization)
306+
logger.debug(
307+
f"Start to list models for admin, user_id: {user_id}, target_tenant_id: {request.tenant_id}, "
308+
f"page: {request.page}, page_size: {request.page_size}")
309+
310+
result = await list_models_for_admin(
311+
request.tenant_id,
312+
request.model_type,
313+
request.page,
314+
request.page_size
315+
)
316+
return JSONResponse(status_code=HTTPStatus.OK, content={
317+
"message": "Successfully retrieved model list",
318+
"data": jsonable_encoder(result)
319+
})
320+
except Exception as e:
321+
logging.error(f"Failed to list models for admin: {str(e)}")
322+
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
323+
detail=str(e))
324+
325+
288326
@router.post("/healthcheck")
289327
async def check_model_health(
290328
display_name: str = Query(..., description="Display name to check"),

backend/consts/model.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,3 +633,44 @@ class InvitationUseResponse(BaseModel):
633633
code_type: str = Field(..., description="Code type")
634634
group_ids: Optional[List[int]] = Field(
635635
None, description="Associated group IDs")
636+
637+
638+
# Admin Model Management Data Models
639+
# ---------------------------------------------------------------------------
640+
class AdminModelListRequest(BaseModel):
641+
"""Request model for admin to list models across tenants"""
642+
tenant_ids: List[str] = Field(
643+
..., min_items=1, description="List of tenant IDs to query")
644+
model_type: Optional[str] = Field(
645+
None, description="Filter by model type (e.g., 'llm', 'embedding')")
646+
page: int = Field(1, ge=1, description="Page number for pagination")
647+
page_size: int = Field(20, ge=1, le=100, description="Items per page")
648+
649+
650+
class TenantModelInfo(BaseModel):
651+
"""Model containing tenant info and their models"""
652+
tenant_id: str = Field(..., description="Tenant identifier")
653+
tenant_name: str = Field(..., description="Tenant display name")
654+
models: List[Dict[str, Any]] = Field(
655+
default_factory=list, description="List of models for this tenant")
656+
657+
658+
class AdminTenantModelRequest(BaseModel):
659+
"""Request model for admin to query models for a specific tenant"""
660+
tenant_id: str = Field(..., min_length=1, description="Target tenant ID to query models for")
661+
model_type: Optional[str] = Field(
662+
None, description="Filter by model type (e.g., 'llm', 'embedding')")
663+
page: int = Field(1, ge=1, description="Page number for pagination")
664+
page_size: int = Field(20, ge=1, le=100, description="Items per page")
665+
666+
667+
class AdminTenantModelResponse(BaseModel):
668+
"""Response model for admin tenant model query"""
669+
tenant_id: str = Field(..., description="Tenant identifier")
670+
tenant_name: str = Field(..., description="Tenant display name")
671+
models: List[Dict[str, Any]] = Field(
672+
default_factory=list, description="List of models for this tenant")
673+
total: int = Field(0, description="Total number of models")
674+
page: int = Field(1, description="Current page number")
675+
page_size: int = Field(20, description="Items per page")
676+
total_pages: int = Field(0, description="Total number of pages")

backend/services/model_management_service.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import List, Dict, Any
2+
from typing import List, Dict, Any, Optional
33

44
from consts.const import LOCALHOST_IP, LOCALHOST_NAME, DOCKER_INTERNAL_HOST
55
from consts.model import ModelConnectStatusEnum
@@ -398,5 +398,84 @@ async def list_llm_models_for_tenant(tenant_id: str):
398398
raise Exception(f"Failed to retrieve model list: {str(e)}")
399399

400400

401+
async def list_models_for_admin(
402+
tenant_id: str,
403+
model_type: Optional[str] = None,
404+
page: int = 1,
405+
page_size: int = 20
406+
) -> Dict[str, Any]:
407+
"""Get models for a specified tenant (admin operation) with pagination.
408+
409+
Args:
410+
tenant_id: Target tenant ID to query models for
411+
model_type: Optional model type filter (e.g., 'llm', 'embedding')
412+
page: Page number for pagination (1-indexed)
413+
page_size: Number of items per page
414+
415+
Returns:
416+
Dict containing tenant_id, tenant_name, paginated models list, and pagination info
417+
"""
418+
try:
419+
# Build filters
420+
filters = None
421+
if model_type:
422+
filters = {"model_type": model_type}
423+
424+
# Get model records for the specified tenant
425+
records = get_model_records(filters, tenant_id)
426+
427+
# Type mapping for backwards compatibility
428+
type_map = {
429+
"chat": "llm",
430+
}
431+
432+
# Normalize model records
433+
normalized_models: List[Dict[str, Any]] = []
434+
for record in records:
435+
record["model_name"] = add_repo_to_name(
436+
model_repo=record["model_repo"],
437+
model_name=record["model_name"],
438+
)
439+
record["connect_status"] = ModelConnectStatusEnum.get_value(
440+
record.get("connect_status"))
441+
442+
# Map model_type if necessary
443+
if record.get("model_type") in type_map:
444+
record["model_type"] = type_map[record["model_type"]]
445+
446+
normalized_models.append(record)
447+
448+
# Calculate pagination
449+
total = len(normalized_models)
450+
total_pages = (total + page_size - 1) // page_size if page_size > 0 else 0
451+
start_index = (page - 1) * page_size
452+
end_index = start_index + page_size
453+
paginated_models = normalized_models[start_index:end_index]
454+
455+
# Get tenant name
456+
from services.tenant_service import get_tenant_info
457+
try:
458+
tenant_info = get_tenant_info(tenant_id)
459+
tenant_name = tenant_info.get("tenant_name", "")
460+
except Exception:
461+
tenant_name = ""
462+
463+
result = {
464+
"tenant_id": tenant_id,
465+
"tenant_name": tenant_name,
466+
"models": paginated_models,
467+
"total": total,
468+
"page": page,
469+
"page_size": page_size,
470+
"total_pages": total_pages
471+
}
472+
473+
logging.debug(f"Successfully retrieved admin model list for tenant: {tenant_id}, page: {page}, page_size: {page_size}")
474+
return result
475+
except Exception as e:
476+
logging.error(f"Failed to retrieve admin model list: {str(e)}")
477+
raise Exception(f"Failed to retrieve admin model list: {str(e)}")
478+
479+
401480

402481

backend/services/tenant_service.py

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,22 @@ def get_tenant_info(tenant_id: str) -> Dict[str, Any]:
2323
"""
2424
Get tenant information by tenant ID
2525
26+
If TENANT_NAME config is missing, automatically create one with default name.
27+
2628
Args:
2729
tenant_id (str): Tenant ID
2830
2931
Returns:
3032
Dict[str, Any]: Tenant information
31-
32-
Raises:
33-
NotFoundException: When tenant not found
3433
"""
3534
# Get tenant name
3635
name_config = get_single_config_info(tenant_id, TENANT_NAME)
3736
if not name_config:
38-
logging.warning(f"The name of tenant {tenant_id} not found.")
37+
logger.warning(f"The name of tenant {tenant_id} not found, creating default config.")
38+
# Auto-create TENANT_NAME config with default name
39+
_ensure_tenant_name_config(tenant_id)
40+
# Re-fetch after creation
41+
name_config = get_single_config_info(tenant_id, TENANT_NAME)
3942

4043
group_config = get_single_config_info(tenant_id, DEFAULT_GROUP_ID)
4144

@@ -48,6 +51,38 @@ def get_tenant_info(tenant_id: str) -> Dict[str, Any]:
4851
return tenant_info
4952

5053

54+
def _ensure_tenant_name_config(tenant_id: str) -> bool:
55+
"""
56+
Ensure TENANT_NAME config exists for the tenant.
57+
Creates a default name config if it doesn't exist.
58+
59+
Args:
60+
tenant_id: Tenant ID
61+
62+
Returns:
63+
bool: True if config exists or was created successfully, False otherwise
64+
"""
65+
# Check if already exists (double-check in case of race condition)
66+
existing = get_single_config_info(tenant_id, TENANT_NAME)
67+
if existing:
68+
return True
69+
70+
# Create default TENANT_NAME config
71+
tenant_name_data = {
72+
"tenant_id": tenant_id,
73+
"config_key": TENANT_NAME,
74+
"config_value": "Unnamed Tenant",
75+
"created_by": "system_auto_create",
76+
"updated_by": "system_auto_create"
77+
}
78+
success = insert_config(tenant_name_data)
79+
if success:
80+
logger.info(f"Auto-created TENANT_NAME config for tenant {tenant_id}")
81+
else:
82+
logger.error(f"Failed to auto-create TENANT_NAME config for tenant {tenant_id}")
83+
return success
84+
85+
5186
def get_all_tenants() -> List[Dict[str, Any]]:
5287
"""
5388
Get all tenants
@@ -163,6 +198,8 @@ def update_tenant_info(tenant_id: str, tenant_name: str, updated_by: Optional[st
163198
"""
164199
Update tenant information
165200
201+
If TENANT_NAME config doesn't exist, creates it with the provided name.
202+
166203
Args:
167204
tenant_id (str): Tenant ID
168205
tenant_name (str): New tenant name
@@ -172,25 +209,35 @@ def update_tenant_info(tenant_id: str, tenant_name: str, updated_by: Optional[st
172209
Dict[str, Any]: Updated tenant information
173210
174211
Raises:
175-
NotFoundException: When tenant not found
176-
ValidationError: When tenant name is invalid
212+
ValidationError: When tenant name is invalid or update fails
177213
"""
178-
# Check if tenant exists and get current name config
179-
name_config = get_single_config_info(tenant_id, TENANT_NAME)
180-
if not name_config:
181-
raise NotFoundException(f"Tenant {tenant_id} not found")
182-
183214
# Validate tenant name
184215
if not tenant_name or not tenant_name.strip():
185216
raise ValidationError("Tenant name cannot be empty")
186217

187-
# Update tenant name
188-
success = update_config_by_tenant_config_id(
189-
name_config["tenant_config_id"],
190-
tenant_name.strip()
191-
)
192-
if not success:
193-
raise ValidationError("Failed to update tenant name")
218+
# Check if tenant name config exists
219+
name_config = get_single_config_info(tenant_id, TENANT_NAME)
220+
if not name_config:
221+
# Tenant config doesn't exist, create it with the provided name
222+
logger.info(f"TENANT_NAME config not found for {tenant_id}, creating new config.")
223+
tenant_name_data = {
224+
"tenant_id": tenant_id,
225+
"config_key": TENANT_NAME,
226+
"config_value": tenant_name.strip(),
227+
"created_by": updated_by,
228+
"updated_by": updated_by
229+
}
230+
success = insert_config(tenant_name_data)
231+
if not success:
232+
raise ValidationError("Failed to create tenant name configuration")
233+
else:
234+
# Update existing config
235+
success = update_config_by_tenant_config_id(
236+
name_config["tenant_config_id"],
237+
tenant_name.strip()
238+
)
239+
if not success:
240+
raise ValidationError("Failed to update tenant name")
194241

195242
# Return updated tenant information
196243
updated_tenant = get_tenant_info(tenant_id)

docker/init.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,7 @@ INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_
820820
(6, 'SU', 'VISIBILITY', 'LEFT_NAV_MENU', '/models'),
821821
(7, 'SU', 'VISIBILITY', 'LEFT_NAV_MENU', '/memory'),
822822
(8, 'SU', 'VISIBILITY', 'LEFT_NAV_MENU', '/users'),
823+
(211, 'SU', 'VISIBILITY', 'LEFT_NAV_MENU', '/tenant-resources'),
823824
(9, 'SU', 'RESOURCE', 'AGENT', 'READ'),
824825
(10, 'SU', 'RESOURCE', 'AGENT', 'DELETE'),
825826
(11, 'SU', 'RESOURCE', 'KB', 'READ'),
@@ -846,6 +847,7 @@ INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_
846847
(32, 'SU', 'RESOURCE', 'TENANT', 'READ'),
847848
(33, 'SU', 'RESOURCE', 'TENANT', 'UPDATE'),
848849
(34, 'SU', 'RESOURCE', 'TENANT', 'DELETE'),
850+
(213, 'SU', 'RESOURCE', 'TENANT.LIST', 'READ'),
849851
(35, 'SU', 'RESOURCE', 'TENANT.INFO', 'READ'),
850852
(36, 'SU', 'RESOURCE', 'TENANT.INFO', 'UPDATE'),
851853
(37, 'SU', 'RESOURCE', 'TENANT.INVITE', 'CREATE'),
@@ -868,6 +870,7 @@ INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_
868870
(54, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/models'),
869871
(55, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/memory'),
870872
(56, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/users'),
873+
(212, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/tenant-resources'),
871874
(57, 'ADMIN', 'RESOURCE', 'AGENT', 'CREATE'),
872875
(58, 'ADMIN', 'RESOURCE', 'AGENT', 'READ'),
873876
(59, 'ADMIN', 'RESOURCE', 'AGENT', 'UPDATE'),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- =============================================================================
2+
-- File: v1.8.0_0202_role_permission_update.sql
3+
-- Description: Add /tenant-resources route permission and tenant list visibility for SU role
4+
-- Version: 1.8.0
5+
-- Date: 2026-02-02
6+
-- =============================================================================
7+
8+
-- Add /tenant-resources LEFT_NAV_MENU permission for SU (Super Admin) role
9+
INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype)
10+
VALUES (211, 'SU', 'VISIBILITY', 'LEFT_NAV_MENU', '/tenant-resources')
11+
ON CONFLICT (role_permission_id) DO NOTHING;
12+
13+
-- Add /tenant-resources LEFT_NAV_MENU permission for ADMIN (Tenant Admin) role
14+
INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype)
15+
VALUES (212, 'ADMIN', 'VISIBILITY', 'LEFT_NAV_MENU', '/tenant-resources')
16+
ON CONFLICT (role_permission_id) DO NOTHING;
17+
18+
-- Add tenant list visibility permission for SU (Super Admin) role - controls tenant list display in resource page
19+
INSERT INTO nexent.role_permission_t (role_permission_id, user_role, permission_category, permission_type, permission_subtype)
20+
VALUES (213, 'SU', 'RESOURCE', 'TENANT.LIST', 'READ')
21+
ON CONFLICT (role_permission_id) DO NOTHING;

frontend/app/[locale]/agents/components/agentInfo/AgentGenerateDetail.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import {
3232
GENERATE_PROMPT_STREAM_TYPES,
3333
} from "@/const/agentConfig";
3434
import { generatePromptStream } from "@/services/promptService";
35-
import { useAuth } from "@/hooks/useAuth";
35+
import { useAuthorizationContext } from "@/components/providers/AuthorizationProvider";
36+
import { useDeployment } from "@/components/providers/deploymentProvider";
3637
import { useModelList } from "@/hooks/model/useModelList";
3738
import ExpandEditModal from "./ExpandEditModal";
3839

@@ -59,7 +60,8 @@ export default function AgentGenerateDetail({
5960
}: AgentGenerateDetailProps) {
6061
const { t } = useTranslation("common");
6162
const { message } = App.useApp();
62-
const { user, isSpeedMode } = useAuth();
63+
const { user } = useAuthorizationContext();
64+
const { isSpeedMode } = useDeployment();
6365
const [form] = Form.useForm();
6466

6567
// Model data from React Query

0 commit comments

Comments
 (0)