-
Notifications
You must be signed in to change notification settings - Fork 689
UN-3815 [FIX] Apply organization scoping to prompt-studio child models #2213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
447d0c9
09d320b
14f94cd
18a53f9
14b7e68
0dce94e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,9 +11,10 @@ | |
| from api_v2.models import APIDeployment | ||
| from celery import signature | ||
| from celery.result import AsyncResult | ||
| from django.db import IntegrityError | ||
| from django.db import IntegrityError, transaction | ||
| from django.db.models import Count, OuterRef, QuerySet, Subquery | ||
| from django.http import HttpRequest, HttpResponse | ||
| from django.shortcuts import get_object_or_404 | ||
| from django.utils import timezone | ||
| from file_management.constants import FileInformationKey as FileKey | ||
| from file_management.exceptions import FileNotFound | ||
|
|
@@ -445,13 +446,24 @@ def make_profile_default(self, request: HttpRequest, pk: Any = None) -> Response | |
| self.get_object() | ||
| ) # Assuming you have a get_object method in your viewset | ||
|
|
||
| ProfileManager.objects.filter(prompt_studio_tool=prompt_tool).update( | ||
| is_default=False | ||
| # Resolve the target before clearing anything: the id comes straight | ||
| # from the request body, and clearing first would leave the tool with no | ||
| # default at all when it does not match. Scoped to the same tool the | ||
| # caller already passed authz on, so another tool's id is a 404. | ||
| profile_manager = get_object_or_404( | ||
| ProfileManager, | ||
| pk=request.data["default_profile"], | ||
| prompt_studio_tool=prompt_tool, | ||
|
Comment on lines
+453
to
+456
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Medium] [Lens 3] — The two malformed-input cases next to the hardened one are still 500s, and the full-object (a) (b) Suggested fix: validate with a small serializer (or Lens 3 |
||
| ) | ||
|
|
||
| profile_manager = ProfileManager.objects.get(pk=request.data["default_profile"]) | ||
| profile_manager.is_default = True | ||
| profile_manager.save() | ||
| # Both writes in one transaction so a failure between them cannot leave | ||
| # the tool with zero defaults or two. | ||
| with transaction.atomic(): | ||
| ProfileManager.objects.filter(prompt_studio_tool=prompt_tool).update( | ||
| is_default=False | ||
| ) | ||
| profile_manager.is_default = True | ||
| profile_manager.save() | ||
|
|
||
| return Response( | ||
| status=status.HTTP_200_OK, | ||
|
|
@@ -1186,7 +1198,13 @@ def delete_for_ide(self, request: HttpRequest, pk: uuid) -> Response: | |
| document_id: str = serializer.validated_data.get(ToolStudioPromptKeys.DOCUMENT_ID) | ||
| org_id = UserSessionUtils.get_organization_id(request) | ||
| user_id = custom_tool.created_by.user_id | ||
| document: DocumentManager = DocumentManager.objects.get(pk=document_id) | ||
| # Scope to the tool the caller already passed authz on — tighter than | ||
| # org scope, and this action never runs filter_queryset(). | ||
| # get_object_or_404 keeps a non-matching id a 404 rather than an | ||
| # unhandled DoesNotExist, which the DRF handler turns into a 500. | ||
| document: DocumentManager = get_object_or_404( | ||
| DocumentManager, pk=document_id, tool=custom_tool | ||
| ) | ||
|
Comment on lines
+1201
to
+1207
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Medium] [Lens 3, 10] — The scoping tightening on this lookup is correct. The issue is the code immediately after it.
The document row and the file are gone, but the Redis indexing flags persist — so a re-upload of the same file is treated as already-indexed. The user is told the delete succeeded, and nothing distinguishes "this document had no index managers" from "the filter hid them". The Suggested fix: log at WARNING when Lens 3 · 10 |
||
|
|
||
| try: | ||
| # Delete indexed flags in redis | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,18 @@ | |
| from account_v2.models import User | ||
| from django.db import models | ||
| from utils.models.base_model import BaseModel | ||
| from utils.models.org_aware_manager import OrgAwareManager | ||
|
|
||
| from prompt_studio.prompt_studio_core_v2.models import CustomTool | ||
|
|
||
|
|
||
| class DocumentManager(BaseModel): | ||
| """Model to store the document details.""" | ||
|
|
||
| # Org scoping lives here because custom @action methods never call | ||
| # filter_queryset(), so OrganizationFilterBackend does not run on them. | ||
|
Comment on lines
+14
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Medium] [Lens 16] — "custom This claim is the PR's stated premise and is repeated at seven sites: here, The two actions this PR fixes do run the filter backend. What actually bypasses the backend is the raw Failure mode: a maintainer reads this as "the filter backend does not apply inside any Suggested fix: reword once and reference it from the other six sites — something like "the filter backend only scopes querysets routed through Lens 16 |
||
| objects = OrgAwareManager() | ||
|
|
||
| document_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | ||
|
|
||
| document_name = models.CharField( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -109,12 +109,15 @@ def mark_extraction_status( | |
|
|
||
| # Lock the row (or create an empty one) so concurrent callers | ||
| # merge into the same dict rather than clobbering each other. | ||
| index_manager, created = ( | ||
| IndexManager.objects.select_for_update().get_or_create( | ||
| document_manager=document, | ||
| profile_manager=profile_manager, | ||
| defaults={"extraction_status": {}}, | ||
| ) | ||
| # of=("self",) because the org-scoped manager joins through | ||
| # DocumentManager and CustomTool; without it Postgres locks | ||
| # rows in those tables too. | ||
| index_manager, created = IndexManager.objects.select_for_update( | ||
| of=("self",) | ||
| ).get_or_create( | ||
| document_manager=document, | ||
|
Comment on lines
+113
to
+118
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Medium] [Lens 3, 5] — Django applies the manager's Reachable two ways: an org-context mismatch between the caller and the row, or a The PR description acknowledges this shape — "If the Same pattern at Suggested fix: route these Lens 3 · 5 |
||
| profile_manager=profile_manager, | ||
| defaults={"extraction_status": {}}, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [High] [Lens 3, 10] — Making this manager org-scoped adds new failure modes to this function, and the existing error handling converts all of them into a silent no-op. Three stages:
Net effect: The bare Suggested fix: return 500 (or 404 for the Lens 3 · 10 |
||
| ) | ||
|
|
||
| # Merge in place — update_or_create(defaults=...) would replace | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| import logging | ||
| from typing import Any | ||
|
|
||
| from django.core.exceptions import ObjectDoesNotExist | ||
| from django.db.models import QuerySet | ||
| from django.http import HttpRequest | ||
| from rest_framework import status, viewsets | ||
|
|
@@ -119,17 +118,22 @@ def get_output_for_tool_default(self, request: HttpRequest) -> Response: | |
| tool_id = request.GET.get("tool_id") | ||
| document_manager_id = request.GET.get("document_manager") | ||
| tool_validation_message = PromptOutputManagerErrorMessage.TOOL_VALIDATION | ||
| tool_not_found = PromptOutputManagerErrorMessage.TOOL_NOT_FOUND | ||
| if not tool_id: | ||
| raise ValidationError(detail=tool_validation_message) | ||
|
|
||
| try: | ||
| # Fetch ToolStudioPrompt records based on tool_id | ||
| tool_studio_prompts = ToolStudioPrompt.objects.filter( | ||
| tool_id=tool_id | ||
| ).order_by("sequence_number") | ||
| except ObjectDoesNotExist: | ||
| raise ValidationError(detail=tool_not_found) | ||
| # Fetch ToolStudioPrompt records based on tool_id. | ||
| # Custom actions skip filter_queryset(), so OrganizationFilterBackend | ||
| # never runs — scope explicitly to prevent cross-tenant reads. | ||
| # | ||
| # No exception handling here: filter() does not raise for a missing or | ||
| # out-of-org tool, it returns empty. Empty is also the correct result | ||
| # for a tool that simply has no prompts yet, which is the normal state | ||
| # of a newly created project — so this stays a 200 with an empty body | ||
| # rather than a validation error. | ||
| tool_studio_prompts = ToolStudioPrompt.objects.filter( | ||
| tool_id=tool_id, | ||
| tool_id__organization=UserContext.get_organization(), | ||
| ).order_by("sequence_number") | ||
|
Comment on lines
+128
to
+136
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Medium] [Lens 3, 16] — Two claims in this comment do not hold CodeRabbit already ran the empty-200 discussion on this endpoint and you answered it; CodeRabbit agreed and I am not reopening that. These are the two parts the thread did not cover. 1. " This is not a regression — the old 2. There is a third cause of empty that the comment does not list. Endpoint is only reachable under Suggested fix: narrow claim 1 to "does not raise for a valid UUID that matches no row", or validate Lens 3 · 16 |
||
|
|
||
| # Invoke helper method to frame and fetch default response. | ||
| result: dict[str, Any] = OutputManagerHelper.fetch_default_output_response( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Medium] [Lens 3, 10] — The
exceptbelow this now conflates "row is filtered out" with "row does not exist", and never self-healsThe
of=("self",)change is correct, and its comment correctly identifies that the org-scoped manager joins throughAdapterInstance. But the handler immediately below was not revisited for the same reason.ProfileManager.objectsis now scoped throughvector_store__organization. A summarize profile that exists but falls outside that scope raisesObjectDoesNotExist, which is caught at:68and reported as:"Filtered out" and "does not exist" become the same INFO line and the same silent no-op. The tool keeps
summarize_llm_adapter = NULL, so summarization keeps using the deprecated profile path — and because this lazy migration re-runs and re-skips on every invocation, it never self-heals and never escalates.The outer
except Exceptionat:93-99is worse: it logs at WARNING and says "Continuing with the deprecated approach for now" — an explicit, undocumented fallback to legacy behaviour on any error, now including theImproperlyConfiguredthatOrgAwareManagerraises when a pin is wrong.Suggested fix: distinguish the two cases — check
ProfileManager._base_manager.filter(prompt_studio_tool=tool_instance, is_summarize_llm=True).exists()before concluding "none found", and log at ERROR with the tool id and current org context when the row exists but is not visible.Lens 3 · 10