P1: 全局JWT保护 + 审计日志补全 — scripts/assets/servers
- scripts.py: 所有9个端点添加JWT认证 + CUD操作审计日志 - assets.py: 所有GET端点添加JWT + 写操作审计日志补全 - servers.py: GET端点JWT保护(上轮未提交) - WSL全量验证通过: ALL CHECKS PASSED
This commit is contained in:
+61
-4
@@ -1,5 +1,5 @@
|
||||
"""Nexus — Platform & Node API Routes (Asset organization layer)
|
||||
All write operations require JWT authentication.
|
||||
All operations require JWT authentication.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
@@ -20,7 +20,10 @@ router = APIRouter(prefix="/api/assets", tags=["assets"])
|
||||
# ── Platforms ──
|
||||
|
||||
@router.get("/platforms", response_model=list)
|
||||
async def list_platforms(db: AsyncSession = Depends(get_db)):
|
||||
async def list_platforms(
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List all platform templates"""
|
||||
repo = PlatformRepositoryImpl(db)
|
||||
platforms = await repo.get_all()
|
||||
@@ -28,7 +31,11 @@ async def list_platforms(db: AsyncSession = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/platforms/{id}", response_model=dict)
|
||||
async def get_platform(id: int, db: AsyncSession = Depends(get_db)):
|
||||
async def get_platform(
|
||||
id: int,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get a single platform by ID"""
|
||||
repo = PlatformRepositoryImpl(db)
|
||||
platform = await repo.get_by_id(id)
|
||||
@@ -74,6 +81,14 @@ async def update_platform(
|
||||
if hasattr(platform, key) and key != "id":
|
||||
setattr(platform, key, value)
|
||||
updated = await repo.update(platform)
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="update_platform",
|
||||
target_type="platform", target_id=id,
|
||||
detail=f"name={updated.name}", ip_address="",
|
||||
))
|
||||
|
||||
return _platform_to_dict(updated)
|
||||
|
||||
|
||||
@@ -85,16 +100,27 @@ async def delete_platform(
|
||||
):
|
||||
"""Delete a platform template"""
|
||||
repo = PlatformRepositoryImpl(db)
|
||||
platform = await repo.get_by_id(id)
|
||||
if not platform:
|
||||
raise HTTPException(status_code=404, detail="Platform not found")
|
||||
result = await repo.delete(id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Platform not found")
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="delete_platform",
|
||||
target_type="platform", target_id=id,
|
||||
detail=f"name={platform.name}", ip_address="",
|
||||
))
|
||||
|
||||
|
||||
# ── Nodes (Tree) ──
|
||||
|
||||
@router.get("/nodes", response_model=list)
|
||||
async def list_nodes(
|
||||
parent_id: Optional[int] = None,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List nodes — all, or children of a specific parent"""
|
||||
@@ -107,7 +133,10 @@ async def list_nodes(
|
||||
|
||||
|
||||
@router.get("/nodes/tree", response_model=list)
|
||||
async def get_node_tree(db: AsyncSession = Depends(get_db)):
|
||||
async def get_node_tree(
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get full node tree structure"""
|
||||
repo = NodeRepositoryImpl(db)
|
||||
all_nodes = await repo.get_all()
|
||||
@@ -124,6 +153,14 @@ async def create_node(
|
||||
repo = NodeRepositoryImpl(db)
|
||||
node = Node(**payload.model_dump(exclude_none=True))
|
||||
created = await repo.create(node)
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="create_node",
|
||||
target_type="node", target_id=created.id,
|
||||
detail=f"name={created.name}", ip_address="",
|
||||
))
|
||||
|
||||
return _node_to_dict(created)
|
||||
|
||||
|
||||
@@ -143,6 +180,14 @@ async def update_node(
|
||||
if hasattr(node, key) and key != "id":
|
||||
setattr(node, key, value)
|
||||
updated = await repo.update(node)
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="update_node",
|
||||
target_type="node", target_id=id,
|
||||
detail=f"name={updated.name}", ip_address="",
|
||||
))
|
||||
|
||||
return _node_to_dict(updated)
|
||||
|
||||
|
||||
@@ -154,10 +199,20 @@ async def delete_node(
|
||||
):
|
||||
"""Delete a node"""
|
||||
repo = NodeRepositoryImpl(db)
|
||||
node = await repo.get_by_id(id)
|
||||
if not node:
|
||||
raise HTTPException(status_code=404, detail="Node not found")
|
||||
result = await repo.delete(id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Node not found")
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="delete_node",
|
||||
target_type="node", target_id=id,
|
||||
detail=f"name={node.name}", ip_address="",
|
||||
))
|
||||
|
||||
|
||||
# ── SSH Sessions & Command Logs ──
|
||||
|
||||
@@ -165,6 +220,7 @@ async def delete_node(
|
||||
async def list_ssh_sessions(
|
||||
server_id: Optional[int] = None,
|
||||
limit: int = 50,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List SSH sessions"""
|
||||
@@ -185,6 +241,7 @@ async def list_command_logs(
|
||||
server_id: Optional[int] = None,
|
||||
session_id: Optional[str] = None,
|
||||
limit: int = 200,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""List command logs"""
|
||||
|
||||
+61
-2
@@ -1,15 +1,19 @@
|
||||
"""Nexus — Scripts API Routes (Script library CRUD + command execution)
|
||||
Presentation layer — receives HTTP requests, delegates to ScriptService.
|
||||
All operations require JWT authentication.
|
||||
"""
|
||||
|
||||
from typing import Optional, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from server.api.dependencies import get_script_service
|
||||
from server.api.dependencies import get_script_service, get_db
|
||||
from server.api.auth_jwt import get_current_admin
|
||||
from server.api.schemas import ScriptCreate, ScriptUpdate, ScriptExecute, DbCredentialCreate
|
||||
from server.application.services.script_service import ScriptService
|
||||
from server.domain.models import Script, DbCredential, Admin
|
||||
from server.domain.models import Script, DbCredential, Admin, AuditLog
|
||||
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
router = APIRouter(prefix="/api/scripts", tags=["scripts"])
|
||||
|
||||
@@ -19,6 +23,7 @@ router = APIRouter(prefix="/api/scripts", tags=["scripts"])
|
||||
@router.get("/", response_model=list)
|
||||
async def list_scripts(
|
||||
category: Optional[str] = Query(None, description="Filter by category"),
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ScriptService = Depends(get_script_service),
|
||||
):
|
||||
"""List all scripts, optionally filtered by category"""
|
||||
@@ -29,6 +34,7 @@ async def list_scripts(
|
||||
@router.get("/{id}", response_model=dict)
|
||||
async def get_script(
|
||||
id: int,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ScriptService = Depends(get_script_service),
|
||||
):
|
||||
"""Get a single script by ID"""
|
||||
@@ -41,11 +47,21 @@ async def get_script(
|
||||
@router.post("/", response_model=dict, status_code=201)
|
||||
async def create_script(
|
||||
payload: ScriptCreate,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ScriptService = Depends(get_script_service),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Create a new script"""
|
||||
script = Script(**payload.model_dump(exclude_none=True))
|
||||
created = await service.create_script(script)
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="create_script",
|
||||
target_type="script", target_id=created.id,
|
||||
detail=f"name={created.name}", ip_address="",
|
||||
))
|
||||
|
||||
return _script_to_dict(created)
|
||||
|
||||
|
||||
@@ -53,7 +69,9 @@ async def create_script(
|
||||
async def update_script(
|
||||
id: int,
|
||||
payload: ScriptUpdate,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ScriptService = Depends(get_script_service),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update an existing script"""
|
||||
script = await service.get_script(id)
|
||||
@@ -63,19 +81,39 @@ async def update_script(
|
||||
if hasattr(script, key) and key != "id":
|
||||
setattr(script, key, value)
|
||||
updated = await service.update_script(script)
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="update_script",
|
||||
target_type="script", target_id=id,
|
||||
detail=f"name={updated.name}", ip_address="",
|
||||
))
|
||||
|
||||
return _script_to_dict(updated)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
async def delete_script(
|
||||
id: int,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ScriptService = Depends(get_script_service),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Delete a script"""
|
||||
script = await service.get_script(id)
|
||||
if not script:
|
||||
raise HTTPException(status_code=404, detail="Script not found")
|
||||
result = await service.delete_script(id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Script not found")
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="delete_script",
|
||||
target_type="script", target_id=id,
|
||||
detail=f"name={script.name}", ip_address="",
|
||||
))
|
||||
|
||||
|
||||
# ── Command Execution ──
|
||||
|
||||
@@ -99,6 +137,7 @@ async def execute_command(
|
||||
@router.get("/executions/{id}", response_model=dict)
|
||||
async def get_execution(
|
||||
id: int,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ScriptService = Depends(get_script_service),
|
||||
):
|
||||
"""Get execution result by ID"""
|
||||
@@ -112,6 +151,7 @@ async def get_execution(
|
||||
|
||||
@router.get("/credentials", response_model=list)
|
||||
async def list_credentials(
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ScriptService = Depends(get_script_service),
|
||||
):
|
||||
"""List all database credentials"""
|
||||
@@ -122,26 +162,45 @@ async def list_credentials(
|
||||
@router.post("/credentials", response_model=dict, status_code=201)
|
||||
async def create_credential(
|
||||
payload: DbCredentialCreate,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ScriptService = Depends(get_script_service),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Create a database credential (password will be encrypted)"""
|
||||
data = payload.model_dump(exclude_none=True)
|
||||
data["encrypted_password"] = data.pop("password", "")
|
||||
credential = DbCredential(**data)
|
||||
created = await service.create_credential(credential)
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="create_credential",
|
||||
target_type="credential", target_id=created.id,
|
||||
detail=f"name={created.name}", ip_address="",
|
||||
))
|
||||
|
||||
return _credential_to_dict(created)
|
||||
|
||||
|
||||
@router.delete("/credentials/{id}", status_code=204)
|
||||
async def delete_credential(
|
||||
id: int,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ScriptService = Depends(get_script_service),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Delete a database credential"""
|
||||
result = await service.delete_credential(id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Credential not found")
|
||||
|
||||
audit_repo = AuditLogRepositoryImpl(db)
|
||||
await audit_repo.create(AuditLog(
|
||||
admin_username=admin.username, action="delete_credential",
|
||||
target_type="credential", target_id=id,
|
||||
ip_address="",
|
||||
))
|
||||
|
||||
|
||||
# ── Helper functions ──
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ async def list_servers(
|
||||
category: Optional[str] = Query(None, description="Filter by category"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
per_page: int = Query(50, ge=1, le=200, description="Items per page"),
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ServerService = Depends(get_server_service),
|
||||
):
|
||||
"""List servers with live status from Redis (paginated)
|
||||
@@ -87,6 +88,7 @@ async def list_servers(
|
||||
|
||||
@router.get("/stats", response_model=dict)
|
||||
async def server_stats(
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ServerService = Depends(get_server_service),
|
||||
):
|
||||
"""Aggregated server stats for dashboard — efficient single query"""
|
||||
@@ -128,6 +130,7 @@ async def server_stats(
|
||||
@router.get("/{id}", response_model=dict)
|
||||
async def get_server(
|
||||
id: int,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ServerService = Depends(get_server_service),
|
||||
):
|
||||
"""Get a single server by ID with live status from Redis"""
|
||||
@@ -242,6 +245,7 @@ async def delete_server(
|
||||
@router.post("/check", response_model=dict)
|
||||
async def check_servers(
|
||||
payload: ServerCheck,
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
service: ServerService = Depends(get_server_service),
|
||||
):
|
||||
"""Check health status of specified servers (Agent-based, no SSH)"""
|
||||
@@ -273,6 +277,7 @@ async def push_to_servers(
|
||||
@router.get("/logs", response_model=list)
|
||||
async def get_all_sync_logs(
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get recent sync logs across all servers (for dashboard charts)"""
|
||||
@@ -287,6 +292,7 @@ async def get_all_sync_logs(
|
||||
async def get_server_logs(
|
||||
id: int,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
admin: Admin = Depends(get_current_admin),
|
||||
sync_service: SyncService = Depends(get_sync_service),
|
||||
):
|
||||
"""Get sync logs for a specific server"""
|
||||
|
||||
Reference in New Issue
Block a user