Files

594 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
import traceback
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from gvsdsdk.msync.auth import (
AuthenticateService,
RefreshServiceToken,
ValidateAccessToken,
)
from gvsdsdk.msync.middleware import ServiceTokenAuthentication
from gvsdsdk.msync.registry import ServiceRegistry, RegistryError
from gvsdsdk.msync.ogm import OGMManager, OGMError
from gvsdsdk.msync.transaction import TransactionManager, TransactionError
from gvsdsdk.msync.local_tx import local_tx
from gvsdsdk.msync.serializers import (
AuthTokenRequestSerializer,
RefreshTokenRequestSerializer,
ServiceRegisterSerializer,
ServiceNodeSerializer,
OGMRegisterSerializer,
OGMBatchRegisterSerializer,
ServiceTokenResponseSerializer,
TransactionCreateSerializer,
TransactionExecuteSerializer,
TransactionRetryStepSerializer,
LocalExecuteSerializer,
LocalCompensateSerializer,
)
logger = logging.getLogger(__name__)
def _ok(data=None, msg='success'):
return {'code': 200, 'msg': msg, 'data': data}
def _err(msg, code=400):
return {'code': code, 'msg': msg, 'data': None}
class AuthTokenView(APIView):
authentication_classes = []
permission_classes = []
def post(self, request):
try:
ser = AuthTokenRequestSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
token = AuthenticateService(
service_name=ser.ValidatedData['ServiceName'],
signed_payload=ser.ValidatedData['Payload'],
signature=ser.ValidatedData['Signature'],
)
if token is None:
return Response(_err('认证失败', 401), status=401)
try:
scope = token.TokenScope
if not isinstance(scope, list):
scope = list(scope) if scope else []
except Exception:
scope = []
try:
# naive UTC 需先补时区再 timestamp(),避免按本地时区解释偏移
if isinstance(token.ExpiresAt, str):
from django.utils.dateparse import parse_datetime
dt = parse_datetime(token.ExpiresAt)
if dt is None:
raise ValueError
else:
dt = token.ExpiresAt
if dt.tzinfo is None:
import datetime as _dt
dt = dt.replace(tzinfo=_dt.timezone.utc)
expires_ts = dt.timestamp()
except Exception:
import time as _time
expires_ts = _time.time() + 7200
data = ServiceTokenResponseSerializer({
'AccessToken': token.AccessToken,
'RefreshToken': token.RefreshToken,
'Scope': scope,
'ExpiresAt': expires_ts,
}).data
return Response(_ok(data))
except Exception as e:
logger.error(f'认证令牌签发异常: {e}\n{traceback.format_exc()}')
return Response(_err('服务器内部错误', 500), status=500)
class AuthTokenRefreshView(APIView):
authentication_classes = []
permission_classes = []
def post(self, request):
try:
ser = RefreshTokenRequestSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
token = RefreshServiceToken(ser.ValidatedData['RefreshToken'])
if token is None:
return Response(_err('刷新失败', 401), status=401)
try:
scope = token.TokenScope
if not isinstance(scope, list):
scope = list(scope) if scope else []
except Exception:
scope = []
try:
# naive UTC 需先补时区再 timestamp(),避免按本地时区解释偏移
if isinstance(token.ExpiresAt, str):
from django.utils.dateparse import parse_datetime
dt = parse_datetime(token.ExpiresAt)
if dt is None:
raise ValueError
else:
dt = token.ExpiresAt
if dt.tzinfo is None:
import datetime as _dt
dt = dt.replace(tzinfo=_dt.timezone.utc)
expires_ts = dt.timestamp()
except Exception:
import time as _time
expires_ts = _time.time() + 7200
data = ServiceTokenResponseSerializer({
'AccessToken': token.AccessToken,
'RefreshToken': token.RefreshToken,
'Scope': scope,
'ExpiresAt': expires_ts,
}).data
return Response(_ok(data))
except Exception as e:
logger.error(f'令牌刷新异常: {e}\n{traceback.format_exc()}')
return Response(_err('服务器内部错误', 500), status=500)
class AuthTokenIntrospectView(APIView):
"""Token 自省端点 — 供子服务器验证 ServiceToken 有效性
子服务器无法直接访问主服务器的 Django ORM 和 MASTER_SECRET,
因此通过此端点远程验证 token。
"""
authentication_classes = []
permission_classes = []
def post(self, request):
access_token = request.data.get('AccessToken', '')
if not access_token:
return Response(_err('AccessToken 不能为空', 400), status=400)
try:
payload = ValidateAccessToken(access_token)
if payload is None:
return Response({'Valid': False, 'Active': False})
return Response({'Valid': True, 'Active': True, 'Payload': payload})
except Exception as e:
logger.error(f'Token 自省异常: {e}\n{traceback.format_exc()}')
return Response({'Valid': False, 'Active': False})
class UserTokenIntrospectView(APIView):
"""用户令牌自省端点 — 供子服务器验证用户级令牌(iframe 身份透传)
入参:AccessToken(用户令牌)、AudienceService(期望目标服务,可选)
校验:签名/过期/typ=user/aud 匹配;返回用户上下文 {UserUUID, RoleUUID,
CompanyUUID, TenantUUID},子服务器按此构建请求用户。
"""
authentication_classes = []
permission_classes = []
def post(self, request):
from gvsdsdk.msync.auth import IntrospectUserToken
access_token = request.data.get('AccessToken', '')
if not access_token:
return Response(_err('AccessToken 不能为空', 400), status=400)
try:
ctx = IntrospectUserToken(
access_token,
AudienceService=request.data.get('AudienceService') or None,
)
if ctx is None:
return Response({'Valid': False, 'Active': False})
return Response({'Valid': True, 'Active': True, 'Payload': ctx})
except Exception as e:
logger.error(f'用户令牌自省异常: {e}\n{traceback.format_exc()}')
return Response({'Valid': False, 'Active': False})
class RegistryListView(APIView):
authentication_classes = [ServiceTokenAuthentication]
def get(self, request):
if not request.user.is_authenticated:
return Response(_err('需要服务认证', 401), status=401)
services = ServiceRegistry.DiscoverAll(active_only=True)
return Response(_ok(services))
def post(self, request):
# 注册端点:新服务尚未拥有令牌,无需认证,但必须携带密钥
# TODO: 考虑添加速率限制以防止滥用注册
ser = ServiceRegisterSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
try:
node = ServiceRegistry.Register(
name=ser.ValidatedData['ServiceName'],
domain=ser.ValidatedData['ServiceDomain'],
secret_hash=ser.ValidatedData.get('ServiceSecretHash', ''),
raw_secret=ser.ValidatedData.get('ServiceSecret'),
scope=ser.ValidatedData.get('ServiceScope'),
service_type=ser.ValidatedData.get('ServiceType', 0),
metadata=ser.ValidatedData.get('ServiceMetadata', {}),
)
except RegistryError as e:
return Response(_err(str(e), 409), status=409)
data = ServiceNodeSerializer(node).data
return Response(_ok(data), status=201)
class RegistryDetailView(APIView):
authentication_classes = [ServiceTokenAuthentication]
def get(self, request, name):
if not request.user.is_authenticated:
return Response(_err('需要服务认证', 401), status=401)
info = ServiceRegistry.Discover(name)
if info is None:
return Response(_err('服务不存在', 404), status=404)
return Response(_ok(info))
class RegistryHeartbeatView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, name):
# 仅允许持有该服务密钥签发的令牌执行本服务心跳
if getattr(request, 'service_name', None) != name:
return Response(_err('无权操作其他服务', 403), status=403)
try:
ServiceRegistry.Heartbeat(name)
except RegistryError as e:
return Response(_err(str(e), 404), status=404)
return Response(_ok(msg='心跳成功'))
class RegistryDeregisterView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def delete(self, request, name):
# 仅允许持有该服务密钥签发的令牌注销本服务
if getattr(request, 'service_name', None) != name:
return Response(_err('无权操作其他服务', 403), status=403)
try:
ServiceRegistry.Deregister(name)
except RegistryError as e:
return Response(_err(str(e), 404), status=404)
return Response(_ok(msg='注销成功'))
class OGMRegisterView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
ser = OGMRegisterSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
try:
ogm = OGMManager.Register(
object_type=ser.ValidatedData['ObjectType'],
local_id=ser.ValidatedData['ObjectLocalID'],
service_name=ser.ValidatedData['ServiceName'],
global_id=ser.ValidatedData.get('ObjectGlobalID'),
metadata=ser.ValidatedData.get('Metadata', {}),
)
except OGMError as e:
return Response(_err(str(e), 409), status=409)
return Response(_ok(OGMManager._to_dict(ogm)), status=201)
class OGMBatchRegisterView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
ser = OGMBatchRegisterSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
try:
results = OGMManager.RegisterBatch(
items=ser.ValidatedData['Items'],
service_name=ser.ValidatedData['ServiceName'],
)
except OGMError as e:
return Response(_err(str(e), 409), status=409)
data = [OGMManager._to_dict(ogm) for ogm in results]
return Response(_ok(data), status=201)
class OGMResolveView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, global_id):
info = OGMManager.Resolve(global_id)
if info is None:
return Response(_err('映射不存在', 404), status=404)
return Response(_ok(info))
class OGMQueryView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request):
ObjectType = request.query_params.get('ObjectType')
ServiceName = request.query_params.get('ServiceName')
LocalID = request.query_params.get('LocalID')
if LocalID and ObjectType and ServiceName:
info = OGMManager.ResolveLocal(ObjectType, LocalID, ServiceName)
if info is None:
return Response(_err('映射不存在', 404), status=404)
return Response(_ok(info))
if ObjectType:
results = OGMManager.QueryByType(ObjectType)
return Response(_ok(results))
if ServiceName:
results = OGMManager.QueryByService(ServiceName, object_type=ObjectType)
return Response(_ok(results))
return Response(_err('请提供 ObjectType 或 ServiceName 参数', 400), status=400)
class OGMUpdateView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def put(self, request, global_id):
metadata = request.data.get('Metadata')
ogm = OGMManager.Update(global_id, metadata=metadata)
if ogm is None:
return Response(_err('映射不存在', 404), status=404)
return Response(_ok(OGMManager._to_dict(ogm)))
class OGMDeregisterView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def delete(self, request, global_id):
success = OGMManager.Deregister(global_id)
if not success:
return Response(_err('映射不存在', 404), status=404)
return Response(_ok(msg='注销成功'))
class TransactionCreateView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
ser = TransactionCreateSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
try:
dtx = TransactionManager.Create(
name=ser.ValidatedData['TransactionName'],
steps_data=ser.ValidatedData['Steps'],
description=ser.ValidatedData.get('TransactionDescription', ''),
transaction_type=ser.ValidatedData.get('TransactionType', 'saga'),
initiator_service_name=ser.ValidatedData.get('InitiatorServiceName'),
reference_ogm_uuid=ser.ValidatedData.get('ReferenceOGMUUID'),
payload=ser.ValidatedData.get('Payload'),
timeout_seconds=ser.ValidatedData.get('TimeoutSeconds', 300),
)
except TransactionError as e:
return Response(_err(str(e), 400), status=400)
return Response(_ok(TransactionManager.GetDetail(dtx.TransactionCode)), status=201)
class TransactionExecuteView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, transaction_code):
ser = TransactionExecuteSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
try:
dtx = TransactionManager.Execute(
transaction_code,
access_token=ser.ValidatedData.get('AccessToken'),
)
except TransactionError as e:
return Response(_err(str(e), 400), status=400)
return Response(_ok(TransactionManager.GetDetail(dtx.TransactionCode)))
class TransactionCompensateView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, transaction_code):
access_token = request.data.get('AccessToken')
try:
dtx = TransactionManager.Compensate(
transaction_code,
access_token=access_token,
)
except TransactionError as e:
return Response(_err(str(e), 400), status=400)
return Response(_ok(TransactionManager.GetDetail(dtx.TransactionCode)))
class TransactionCancelView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, transaction_code):
try:
TransactionManager.Cancel(transaction_code)
except TransactionError as e:
return Response(_err(str(e), 400), status=400)
return Response(_ok(msg='事务已取消'))
class TransactionDetailView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, transaction_code):
detail = TransactionManager.GetDetail(transaction_code)
if detail is None:
return Response(_err('事务不存在', 404), status=404)
return Response(_ok(detail))
class TransactionRetryStepView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, transaction_code):
ser = TransactionRetryStepSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
try:
step = TransactionManager.RetryStep(
transaction_code,
step_index=ser.ValidatedData['StepIndex'],
access_token=ser.ValidatedData.get('AccessToken'),
)
except TransactionError as e:
return Response(_err(str(e), 400), status=400)
return Response(_ok({
'StepIndex': step.StepIndex,
'StepName': step.StepName,
'Status': step.get_StepStatus_display(),
'RetryCount': step.RetryCount,
}))
class TransactionRetryCompensateView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request, transaction_code):
ser = TransactionRetryStepSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
try:
step = TransactionManager.RetryCompensate(
transaction_code,
step_index=ser.ValidatedData['StepIndex'],
access_token=ser.ValidatedData.get('AccessToken'),
)
except TransactionError as e:
return Response(_err(str(e), 400), status=400)
return Response(_ok({
'StepIndex': step.StepIndex,
'StepName': step.StepName,
'Status': step.get_StepStatus_display(),
'RetryCount': step.RetryCount,
'TransactionStatus': step.TransactionUUID.get_TransactionStatus_display(),
}))
class TransactionTimeoutCheckView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
token = getattr(request.user, 'service_token', None)
timed_out = TransactionManager.CheckTimeouts(access_token=token)
return Response(_ok({'TimedOut': timed_out, 'Count': len(timed_out)}))
class TransactionQueryView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request):
status = request.query_params.get('status')
if status is None:
return Response(_err('请提供 status 参数', 400), status=400)
try:
status_int = int(status)
except ValueError:
return Response(_err('status 必须为整数', 400), status=400)
results = TransactionManager.QueryByStatus(status_int)
return Response(_ok(results))
class LocalExecuteView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
ser = LocalExecuteSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
result = local_tx.ExecuteStep(
name=ser.ValidatedData['StepName'],
payload=ser.ValidatedData.get('Payload'),
idempotency_key=request.headers.get('Idempotency-Key'),
)
if result['success']:
return Response(_ok(result['result']))
return Response(_err(result['error'], 500), status=500)
class LocalCompensateView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
ser = LocalCompensateSerializer(data=request.data)
if not ser.IsValid():
return Response(_err(ser.errors, 400), status=400)
result = local_tx.CompensateStep(
name=ser.ValidatedData['StepName'],
payload=ser.ValidatedData.get('Payload'),
action_result=ser.ValidatedData.get('ActionResult'),
idempotency_key=request.headers.get('Idempotency-Key'),
)
if result['success']:
return Response(_ok(result['result']))
return Response(_err(result['error'], 500), status=500)
class LocalHandlersView(APIView):
authentication_classes = [ServiceTokenAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request):
handlers = local_tx.ListHandlers()
return Response(_ok(handlers))