Add staff visibility workstation enrollment registration vault and category task views

This commit is contained in:
A R R R Associates
2026-09-02 14:05:20 +05:30
parent 1fe1597d6f
commit 97777e13a1
22 changed files with 538 additions and 103 deletions
+125 -3
View File
@@ -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)