Add staff visibility workstation enrollment registration vault and category task views
This commit is contained in:
+125
-3
@@ -17,6 +17,7 @@ from sqlalchemy.orm import joinedload
|
||||
from app.core.db.common import CommonSessionLocal
|
||||
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
||||
from app.core.security.session_auth import get_current_user
|
||||
from app.core.security.jwt_tokens import decode_token, encode_access_token
|
||||
from app.core.templating import templates
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.core.rbac.permission_guard import require_permission
|
||||
@@ -33,6 +34,7 @@ from app.modules.documents.services import (
|
||||
cleanup_completed_permanent_storage_job_vps_stage,
|
||||
retry_verified_vps_cleanup_for_node,
|
||||
authenticate_storage_node,
|
||||
authenticate_storage_agent,
|
||||
create_branch_storage_node,
|
||||
generate_storage_secret,
|
||||
hash_storage_secret,
|
||||
@@ -1578,6 +1580,122 @@ def download_storage_node_env(request: Request, csrf_token: str = Form(...)):
|
||||
return RedirectResponse(url="/documents/storage-nodes?error=env_download_disabled", status_code=303)
|
||||
|
||||
|
||||
@router.post("/storage-nodes/{node_id}/download-new-workstation")
|
||||
def download_new_workstation_agent(
|
||||
request: Request,
|
||||
node_id: int,
|
||||
storage_root_path: str = Form(r"D:\AuditFirmStorage"),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
"""Download a short-lived enrollment package without rotating existing agent credentials."""
|
||||
validate_csrf(request, csrf_token)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
user, response = _require_user(request, db, "documents.upload")
|
||||
if response:
|
||||
return response
|
||||
scope = build_document_scope(request, db, user)
|
||||
if not _can_manage_branch_storage(scope):
|
||||
return _redirect_denied()
|
||||
node = db.get(BranchStorageNode, node_id)
|
||||
if not _node_allowed_for_storage_scope(node, user, scope):
|
||||
return _redirect_denied()
|
||||
token = encode_access_token(
|
||||
{
|
||||
"purpose": "workstation_enrollment",
|
||||
"node_id": int(node.id),
|
||||
"node_code": node.node_code,
|
||||
"tenant_id": int(node.tenant_id),
|
||||
"branch_id": int(node.branch_id) if node.branch_id is not None else None,
|
||||
},
|
||||
expires_minutes=30,
|
||||
)
|
||||
env_text = build_agent_env(
|
||||
erp_base_url=str(request.base_url).rstrip("/"),
|
||||
node_code=node.node_code,
|
||||
node_secret="",
|
||||
enrollment_token=token,
|
||||
storage_root=_effective_storage_root(node, storage_root_path),
|
||||
tenant_id=node.tenant_id,
|
||||
branch_id=node.branch_id,
|
||||
)
|
||||
package = build_preconfigured_agent_zip(
|
||||
env_text=env_text, include_admin_readme=bool(scope.is_system_admin)
|
||||
)
|
||||
filename = _agent_download_filename(f"{node.node_code}-NEW-WORKSTATION", ".zip")
|
||||
return Response(
|
||||
package,
|
||||
media_type="application/zip",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/storage-agent/enroll-workstation")
|
||||
async def enroll_workstation(request: Request):
|
||||
"""Exchange a short-lived enrollment token for a workstation-only agent secret."""
|
||||
payload = await request.json()
|
||||
token = str(payload.get("enrollment_token") or "").strip()
|
||||
workstation_payload = payload.get("workstation") if isinstance(payload.get("workstation"), dict) else {}
|
||||
try:
|
||||
claims = decode_token(token)
|
||||
except Exception:
|
||||
return JSONResponse({"ok": False, "error": "invalid_or_expired_enrollment_token"}, status_code=401)
|
||||
if claims.get("purpose") != "workstation_enrollment":
|
||||
return JSONResponse({"ok": False, "error": "invalid_enrollment_token_type"}, status_code=401)
|
||||
instance_id = str(workstation_payload.get("agent_instance_id") or "").strip()
|
||||
fingerprint = str(workstation_payload.get("machine_fingerprint") or "").strip()
|
||||
machine_name = str(workstation_payload.get("machine_name") or "").strip()
|
||||
if not instance_id or not fingerprint or not machine_name:
|
||||
return JSONResponse({"ok": False, "error": "workstation_identity_required"}, status_code=400)
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
node = db.get(BranchStorageNode, int(claims.get("node_id") or 0))
|
||||
if (
|
||||
not node
|
||||
or not node.is_active
|
||||
or node.node_code != claims.get("node_code")
|
||||
or int(node.tenant_id) != int(claims.get("tenant_id") or 0)
|
||||
):
|
||||
return JSONResponse({"ok": False, "error": "enrollment_node_unavailable"}, status_code=403)
|
||||
row = db.execute(
|
||||
select(ERPWorkstationAgent).where(
|
||||
ERPWorkstationAgent.storage_node_id == node.id,
|
||||
ERPWorkstationAgent.agent_instance_id == instance_id,
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if row is None:
|
||||
row = ERPWorkstationAgent(
|
||||
tenant_id=node.tenant_id,
|
||||
branch_id=node.branch_id,
|
||||
storage_node_id=node.id,
|
||||
agent_instance_id=instance_id,
|
||||
machine_fingerprint=fingerprint,
|
||||
machine_name=machine_name[:200],
|
||||
)
|
||||
db.add(row)
|
||||
secret = generate_storage_secret()
|
||||
row.workstation_secret_hash = hash_storage_secret(secret)
|
||||
row.machine_fingerprint = fingerprint
|
||||
row.machine_name = machine_name[:200]
|
||||
row.platform_name = str(workstation_payload.get("platform_name") or "")[:120] or None
|
||||
row.agent_version = str(workstation_payload.get("agent_version") or ERP_LOCAL_AGENT_VERSION)[:40] or None
|
||||
row.is_active = True
|
||||
row.status = "enrolled"
|
||||
row.last_seen_at_utc = datetime.now(timezone.utc)
|
||||
row.last_seen_ip = request.client.host if request.client else None
|
||||
db.commit()
|
||||
return {
|
||||
"ok": True,
|
||||
"node_code": node.node_code,
|
||||
"agent_instance_id": instance_id,
|
||||
"workstation_secret": secret,
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.post("/storage-nodes/download-agent-package")
|
||||
def download_preconfigured_storage_agent(
|
||||
request: Request,
|
||||
@@ -1694,7 +1812,10 @@ def storage_jobs(request: Request, status: str = ""):
|
||||
|
||||
|
||||
def _agent_auth(db, request: Request, x_node_code: str | None, x_node_secret: str | None):
|
||||
node = authenticate_storage_node(db, x_node_code, x_node_secret, request=request)
|
||||
instance_id = (request.headers.get("x-agent-instance-id") or "").strip()
|
||||
node = authenticate_storage_agent(
|
||||
db, x_node_code, x_node_secret, agent_instance_id=instance_id, request=request
|
||||
)
|
||||
if not node:
|
||||
return None, JSONResponse({"ok": False, "error": "invalid_storage_node_credentials"}, status_code=401)
|
||||
return node, None
|
||||
@@ -1942,11 +2063,12 @@ async def storage_agent_tunnel(websocket: WebSocket):
|
||||
"""
|
||||
node_code = websocket.query_params.get("node_code") or websocket.headers.get("x-node-code")
|
||||
node_secret = websocket.query_params.get("node_secret") or websocket.headers.get("x-node-secret")
|
||||
agent_instance_id = websocket.query_params.get("agent_instance_id") or websocket.headers.get("x-agent-instance-id")
|
||||
await websocket.accept()
|
||||
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
node = authenticate_storage_node(db, node_code, node_secret, request=None)
|
||||
node = authenticate_storage_agent(db, node_code, node_secret, agent_instance_id=agent_instance_id, request=None)
|
||||
if not node:
|
||||
await websocket.send_json({"ok": False, "type": "error", "error": "invalid_storage_node_credentials"})
|
||||
await websocket.close(code=1008)
|
||||
@@ -1971,7 +2093,7 @@ async def storage_agent_tunnel(websocket: WebSocket):
|
||||
|
||||
db = CommonSessionLocal()
|
||||
try:
|
||||
node = authenticate_storage_node(db, node_code, node_secret, request=None)
|
||||
node = authenticate_storage_agent(db, node_code, node_secret, agent_instance_id=agent_instance_id, request=None)
|
||||
if not node:
|
||||
await websocket.send_json({"ok": False, "type": "error", "error": "node_deactivated_or_invalid"})
|
||||
await websocket.close(code=1008)
|
||||
|
||||
Reference in New Issue
Block a user