ixg_platform/apps/ai/views.py
ixgadmin 8ad83a7a65 Restrict every data API to ops and admin
Batch writes were closed in 88eede5, but every other app's viewset still used
IsAuthenticated, which in this platform means any buyer. A buyer could rewrite a
document record, create a QC result (which moves the batch status and raises a
CAPA), and reach the write endpoints for CAPAs, certificates, ESG reports and
shipments.

- Use the one IsOpsOrAdmin from apps.compliance.permissions; drop the duplicate
  copy in apps/qc/views.py.
- Apply it to the whole viewset on shipments, qc, capa, esg, documents,
  certificates-of-analysis and regulatory-certs. The buyer portal is
  server-rendered through the ORM, so it needs none of these APIs.
- Keep the buyer chat working: AI interactions stay authenticated and scoped,
  and agent reads stay open while agent writes become ops/admin.
- Close a buyer-to-admin escalation in the users API: writes are admin-only and
  role/is_active are read-only, so a buyer can no longer PATCH their own row to
  role=admin and then pass every IsOpsOrAdmin check.
2026-09-17 15:53:44 +00:00

127 lines
4.6 KiB
Python

import uuid
from django.db import models
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from apps.compliance.permissions import IsOpsOrAdmin
from .models import AIInteraction, AgentConfig
from .serializers import (
AIInteractionSerializer, ChatRequestSerializer,
AgentConfigSerializer, AgentConfigWriteSerializer,
)
from .agent_service import AgentService
class AIInteractionViewSet(viewsets.ReadOnlyModelViewSet):
queryset = AIInteraction.objects.all()
serializer_class = AIInteractionSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
qs = AIInteraction.objects.filter(user=self.request.user)
session = self.request.query_params.get("session_id")
if session:
qs = qs.filter(session_id=session)
return qs
@action(detail=False, methods=["post"])
def chat(self, request):
serializer = ChatRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
agent_id = serializer.validated_data.get("agent_id")
message = serializer.validated_data["message"]
session_id = serializer.validated_data.get("session_id") or str(uuid.uuid4())
history = serializer.validated_data.get("history", [])
system_prompt_override = serializer.validated_data.get("system_prompt_override", "")
temperature = serializer.validated_data.get("temperature")
max_tokens = serializer.validated_data.get("max_tokens")
agent = None
if agent_id:
agent = AgentConfig.objects.filter(id=agent_id, is_active=True).first()
else:
agent = AgentConfig.objects.filter(is_default=True, is_active=True).first()
if not agent:
return Response(
{"error": "No active agent found. Create an agent or set one as default."},
status=status.HTTP_400_BAD_REQUEST,
)
AIInteraction.objects.create(
user=request.user,
session_id=session_id,
message_role=AIInteraction.MessageRole.USER,
message_content=message,
)
success, response_text, tokens_used = AgentService.execute_prompt(
config=agent,
prompt=message,
history=history,
system_prompt_override=system_prompt_override or None,
temperature=temperature,
max_tokens=max_tokens,
)
if not success:
return Response(
{"error": response_text, "session_id": session_id},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
AIInteraction.objects.create(
user=request.user,
session_id=session_id,
message_role=AIInteraction.MessageRole.ASSISTANT,
message_content=response_text,
tokens_used=tokens_used or 0,
)
return Response({
"response": response_text,
"session_id": session_id,
"tokens_used": tokens_used,
"agent": agent.name,
})
@action(detail=False, methods=["get"])
def sessions(self, request):
sessions = (
AIInteraction.objects.filter(user=request.user)
.values("session_id")
.distinct()
.order_by("-created_at")[:20]
)
return Response([s["session_id"] for s in sessions if s["session_id"]])
class AgentConfigViewSet(viewsets.ModelViewSet):
queryset = AgentConfig.objects.all()
# The buyer chat reads the agent it talks to — templates/portal/chat_embed.html
# fetches /api/agents/<id>/ — so reads stay open to an authenticated user,
# scoped by get_queryset to their own or the shared agents. Only ops/admin
# may create or change one.
permission_classes = [permissions.IsAuthenticated]
def get_permissions(self):
if self.action in ("create", "update", "partial_update", "destroy"):
return [IsOpsOrAdmin()]
return super().get_permissions()
def get_serializer_class(self):
if self.request.user.role in ("admin", "ops") or self.request.user.is_superuser:
return AgentConfigWriteSerializer
return AgentConfigSerializer
def get_queryset(self):
user = self.request.user
if user.role in ("admin", "ops") or user.is_superuser:
return AgentConfig.objects.all()
return AgentConfig.objects.filter(
models.Q(user=user) | models.Q(user__isnull=True, is_active=True)
)