diff --git a/wwwroot/api/audit.dspy b/wwwroot/api/audit.dspy new file mode 100644 index 0000000..0ec38e0 --- /dev/null +++ b/wwwroot/api/audit.dspy @@ -0,0 +1,71 @@ +# audit.dspy - 审计日志 API +# 查看/备份/删除 仅 owner.audit 角色(审计独立性:superuser 也无权访问) +# action: list / backup / delete + +user_id = await get_user() +if not user_id: + return json.dumps({'ok': False, 'error': '未登录'}, ensure_ascii=False) + +action = (params_kw or {}).get('action', 'list') + +from pipeline_service.audit_service import ( + is_audit_role, list_audit_logs, backup_audit_logs, delete_audit_logs, +) + +# 查 username 和 client_ip +username = user_id +try: + async with get_sor_context(request._run_ns, 'pipeline') as _sor: + _u = await _sor.sqlExe("SELECT username FROM users WHERE id=${u}$", {'u': user_id}) + if _u: + username = getattr(_u[0], 'username', user_id) or user_id +except Exception: + pass + +client_ip = '' +try: + client_ip = request.get('client_ip', '') or '' +except Exception: + pass + +# 权限校验:所有 action 仅 owner.audit(superuser 也不行) +async with get_sor_context(request._run_ns, 'pipeline') as _sor: + if not await is_audit_role(_sor, user_id): + return json.dumps({'ok': False, 'error': '仅 owner.audit 角色可访问审计日志'}, ensure_ascii=False) + +if action == 'list': + filters = { + 'user_id': (params_kw or {}).get('user_id', ''), + 'username': (params_kw or {}).get('username', ''), + 'action': (params_kw or {}).get('act', ''), + 'from': (params_kw or {}).get('from', ''), + 'to': (params_kw or {}).get('to', ''), + } + limit = (params_kw or {}).get('limit', 100) + offset = (params_kw or {}).get('offset', 0) + try: + async with get_sor_context(request._run_ns, 'pipeline') as sor: + rows = await list_audit_logs(sor, filters, limit, offset) + return json.dumps({'ok': True, 'logs': rows}, ensure_ascii=False) + except Exception as e: + return json.dumps({'ok': False, 'error': str(e)}, ensure_ascii=False) + +elif action == 'backup': + try: + async with get_sor_context(request._run_ns, 'pipeline') as sor: + r = await backup_audit_logs(sor, user_id, username, None, client_ip) + return json.dumps(r, ensure_ascii=False) + except Exception as e: + return json.dumps({'ok': False, 'error': str(e)}, ensure_ascii=False) + +elif action == 'delete': + before = (params_kw or {}).get('before', '') + try: + async with get_sor_context(request._run_ns, 'pipeline') as sor: + r = await delete_audit_logs(sor, user_id, username, before, client_ip) + return json.dumps(r, ensure_ascii=False) + except Exception as e: + return json.dumps({'ok': False, 'error': str(e)}, ensure_ascii=False) + +else: + return json.dumps({'ok': False, 'error': '未知 action: ' + str(action)}, ensure_ascii=False) diff --git a/wwwroot/api/work_env.dspy b/wwwroot/api/work_env.dspy index c7c504d..52d5cde 100644 --- a/wwwroot/api/work_env.dspy +++ b/wwwroot/api/work_env.dspy @@ -11,6 +11,13 @@ from pipeline_service.work_env import ( get_work_env, set_work_env, ensure_remote_bwrap, get_org_pubkey, ) from pipeline_service.deploy_account import account_name_for, sanitize_username +from pipeline_service.audit_service import audit_log + +client_ip = '' +try: + client_ip = request.get('client_ip', '') or '' +except Exception: + pass # 查用户 username(账号名 ag_)和 org_id username = user_id @@ -67,6 +74,11 @@ elif action == 'set': try: async with get_sor_context(request._run_ns, 'pipeline') as sor: r = await set_work_env(sor, owner_type, owner_id, mode, remote_config, account_name) + # 审计:工作环境变更(尤其远程主机变更,高风险) + await audit_log(sor, user_id, username, 'work_env_set', + target=owner_type + ':' + str(owner_id), + detail='mode=' + mode + ' host=' + str(remote_config.get('remote_host', '')), + result='ok' if r.get('ok') else 'fail', client_ip=client_ip) return json.dumps(r, ensure_ascii=False) except Exception as e: return json.dumps({'ok': False, 'error': str(e)}, ensure_ascii=False)