UN-2651 [FIX] Show execution logs to group and org-shared users - #2234
UN-2651 [FIX] Show execution logs to group and org-shared users#2234kirtimanmishrazipstack wants to merge 3 commits into
Conversation
Summary by CodeRabbit
WalkthroughThe change updates execution visibility to use user-scoped resource querysets. Execution log access now checks execution visibility before querying logs. Tests cover organization sharing, group sharing, unshared deployments, and denied or allowed log access. ChangesExecution access control
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant RequestingUser
participant WorkflowExecutionLogViewSet
participant WorkflowExecutionManager
participant ExecutionLogs
RequestingUser->>WorkflowExecutionLogViewSet: Request execution logs
WorkflowExecutionLogViewSet->>WorkflowExecutionManager: Check execution access for user
WorkflowExecutionManager-->>WorkflowExecutionLogViewSet: Return accessible execution or no match
WorkflowExecutionLogViewSet->>ExecutionLogs: Query logs for accessible execution
WorkflowExecutionLogViewSet-->>RequestingUser: Return logs or PermissionDenied
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The executions list resolved visibility through direct memberships only, so a deployment reached via a group share or shared_to_org opened fine while its Logs page came back empty. Defer to each resource's own for_user, which spans every sharing path the resource list itself honours (owner, co-owner, direct share, group share, shared_to_org). The per-execution logs and export endpoints had the mirrored problem: no scoping at all. IsOwner sat in permission_classes but implements only has_object_permission, which DRF never invokes on list/export, so any org member holding an execution id could read and CSV-export its logs. Gate the queryset on the executions the caller can see instead, and deny a missing execution the same way as an inaccessible one so the response does not confirm which ids exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
f680ac4 to
5faa618
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/workflow_manager/execution/tests/test_shared_execution_access.py`:
- Around line 98-101: Add a test case alongside
test_logs_denied_when_the_execution_is_not_accessible that calls _log_queryset
with the outsider user and a nonexistent execution ID, and assert it raises
PermissionDenied, preserving the same response as for inaccessible existing
executions.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 11b32fb9-dd05-4444-aaa8-e84fba18d494
📒 Files selected for processing (3)
backend/workflow_manager/execution/tests/test_shared_execution_access.pybackend/workflow_manager/workflow_v2/execution_log_view.pybackend/workflow_manager/workflow_v2/models/execution.py
|
| Filename | Overview |
|---|---|
| backend/workflow_manager/workflow_v2/models/execution.py | Execution visibility now delegates to the organization-scoped, sharing-aware managers for workflows, API deployments, and pipelines. |
| backend/workflow_manager/workflow_v2/execution_log_view.py | Log list and export queries now require the requested execution to be visible to the authenticated user. |
| backend/workflow_manager/execution/tests/test_shared_execution_access.py | Tests cover group and organization sharing, inaccessible and unknown executions, and authorized log access. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
U[Authenticated user] --> L[Request execution logs]
L --> E[WorkflowExecution.objects.for_user]
E --> W[Visible workflow]
E --> A[Visible API deployment]
E --> P[Visible pipeline]
W --> V{Execution visible?}
A --> V
P --> V
V -->|Yes| R[Return or export logs]
V -->|No| D[Permission denied]
Reviews (2): Last reviewed commit: "UN-2651 [FIX] Test that unknown executio..." | Re-trigger Greptile
There was a problem hiding this comment.
🧹 Nitpick comments (1)
backend/workflow_manager/workflow_v2/execution_log_view.py (1)
32-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winResolve or intentionally suppress Ruff RUF012.
Line 32 assigns a mutable list to a class attribute. All viewset instances share this list. Annotate the attribute as
ClassVaror use the repository's approved immutable iterable so the shared permission configuration is explicit. Verify that the chosen form matches the DRF version used by this project.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/workflow_manager/workflow_v2/execution_log_view.py` at line 32, Update the permission_classes class attribute in the viewset to resolve Ruff RUF012 by annotating it with ClassVar or using the repository-approved immutable iterable. Preserve DRF’s expected permission configuration and confirm the chosen form is compatible with the project’s DRF version.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@backend/workflow_manager/workflow_v2/execution_log_view.py`:
- Line 32: Update the permission_classes class attribute in the viewset to
resolve Ruff RUF012 by annotating it with ClassVar or using the
repository-approved immutable iterable. Preserve DRF’s expected permission
configuration and confirm the chosen form is compatible with the project’s DRF
version.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8ba2a165-f055-432f-90a1-aee20ed7414e
📒 Files selected for processing (3)
backend/workflow_manager/execution/tests/test_shared_execution_access.pybackend/workflow_manager/workflow_v2/execution_log_view.pybackend/workflow_manager/workflow_v2/models/execution.py
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/workflow_manager/execution/tests/test_shared_execution_access.py
- backend/workflow_manager/workflow_v2/models/execution.py
…sible ones Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Unstract test resultsPer-group results
Critical paths
|
chandrasekharan-zipstack
left a comment
There was a problem hiding this comment.
Standardized PR Review — INITIAL
Verdict: REQUEST CHANGES
Critical: 0 · High: 4 · Medium: 5 · Low: 3 · Lenses run: 16/16
The change itself is correct and is a net security improvement — it fixes a real intra-org log-read hole and a real visibility bug, and nothing in the diff is a regression. REQUEST CHANGES is for three things: the PR's stated security claim is falsified by a sibling endpoint on the same URL prefix, the identical anti-pattern the PR diagnoses is left in place one file away, and the negative control in the new test suite asserts nothing.
Reviewed at head ef3c01d. Posted as COMMENT, so it carries no merge-gate weight. The 3 Low findings are in a separate follow-up comment.
Unanchored findings
Both of these are High, and both are about files this PR does not touch — so there is no diff line to attach them to. They are the two most consequential findings in this review.
[High] [Lens 1 — Spec & intent, Lens 4 — Security] — /execution/<id>/files/ still returns log text for any execution id
Would anchor to: backend/workflow_manager/file_execution/views.py:14-36
The PR body states: "A run's logs can now only be opened by people the deployment or pipeline was actually shared with." That is not true after this diff.
FileCentricExecutionViewSet is mounted on the same execution/ prefix as the two endpoints being gated — backend/backend/urls_v2.py:65 and :66 — and is fetched by the same UI page. It carries permission_classes = [IsAuthenticated] and keys its queryset on the URL id alone:
execution_id = self.kwargs.get("pk") # views.py:23
return FileExecution.objects.filter(
workflow_execution_id=execution_id # views.py:34
).annotate(latest_log_data=Subquery(latest_log_subquery))No for_user anywhere. Any authenticated org member holding an execution id gets file names, file_path, sizes, statuses, execution_error, and — via get_status_msg (file_execution/serializers.py:36,40) — the latest ExecutionLog.data["log"] string for that run. That is the same data class this PR is protecting, leaking through the sibling endpoint at the same URL depth.
Note the resulting inconsistency: the detail route /execution/<id>/ is gated (workflow_manager/execution/views/execution.py:28-30), so an unauthorized caller now gets 403 on detail and 200 on files.
Suggested fix: the same one-line gate in FileCentricExecutionViewSet.get_queryset. Better still, factor the check into a small mixin shared with WorkflowExecutionLogViewSet, so the next execution/<pk>/<thing>/ route inherits it instead of re-deriving it.
Pre-existing rather than introduced here — but it defeats the PR's own stated goal, so it belongs in this PR or an explicitly linked follow-up. Confidence: High.
[High] [Lens 4 — Security, Lens 2 — Precedent] — GET /workflow/<workflow_id>/execution/ has the identical dead-IsOwner defect, left in place
Would anchor to: backend/workflow_manager/workflow_v2/execution_view.py:13-26
permission_classes = [IsOwner] # :15
...
queryset = WorkflowExecution.objects.filter(workflow_id=workflow_id) # :22-24No for_user. IsOwner implements only has_object_permission (permissions/permission.py:113), which DRF never invokes on list — verbatim the reasoning this PR gives for the log viewset. Any authenticated org member can enumerate every execution of any workflow id in the org (ids, statuses, timings, execution_error), including workflows never shared with them.
The route is live: workflow_v2/urls/workflow.py:79-83 → workflow_manager/urls.py:20 → backend/urls.py:34. DEFAULT_PERMISSION_CLASSES is [] (settings/base.py:643), so [IsOwner] is the entire gate. OrganizationFilterBackend still applies here (this viewset does not override filter_backends), so it is org-bounded but not user-bounded.
Secondary: the class also omits IsAuthenticated — not anonymously exploitable only because auth middleware covers it, which leaves it one settings change from being open.
Suggested fix: WorkflowExecution.objects.for_user(self.request.user).filter(workflow_id=workflow_id), plus IsAuthenticated in permission_classes.
Separately and out of scope: retrieve on this viewset filters workflow_id=pk while pk is an execution id, so execution/<uuid:pk>/ can never resolve — likely a dead route worth a look.
Pre-existing; flagged because leaving it makes this cleanup partial and preserves the exact anti-pattern the diff is retiring. Confidence: High.
Lens checklist (16/16)
| # | Lens | Result |
|---|---|---|
| 1 | Spec & intent | See unanchored finding 1 |
| 2 | Architectural fit & precedent | See execution_log_view.py:32, unanchored finding 2 — the delegation to each resource's own for_user is the right consolidation; the objection is to what was left behind |
| 3 | Correctness & edge cases | See Low findings (follow-up comment) |
| 4 | Security | See both unanchored findings, execution_log_view.py:32 |
| 5 | Data integrity & migrations | N/A — no migration, schema, or persisted-field change |
| 6 | Concurrency | N/A — no locks, threads, async ordering or retries in the diff |
| 7 | API & contract compatibility | Clean — the logs endpoint narrows 200→403 for unauthorized callers and the PR body declares it; no wire-format or field change |
| 8 | Reliability & resilience | N/A — no external calls, timeouts, retries or unbounded buffers in the diff |
| 9 | Performance & cost | See models/execution.py:69-75 |
| 10 | Observability | Clean — the denial is logged via ExceptionLoggingMiddleware.format_exc_and_log (middleware/exception.py:63-66), so the new gate is not a blind spot |
| 11 | Operational safety | N/A — no IaC, Helm, CI or feature-flag change; rollback is a revert |
| 12 | LLM/agent | N/A — no model, prompt, tool or eval code touched |
| 13 | Testing | See test_shared_execution_access.py:42-48, :50-58, :73-80, models/execution.py:66-68, plus Lows |
| 14 | Dependencies & build | N/A — no dependency, lockfile or build change |
| 15 | Code quality | Clean |
| 16 | Doc & comment accuracy | See models/execution.py:37-38 — the two comments this diff adds are both accurate; the defect is a pre-existing line inside the edited block |
Notes
- Nothing was weakened to green CI. No existing test file is touched; no assertion removed or relaxed. The
IsOwnerremoval is a genuine correctness fix, not test-silencing. - I did not run the test suite. The 6/6 claim is unverified by me; none of the findings above depend on a test failing.
- CodeRabbit's one finding (unknown-id denial test) was addressed by the author in
ef3c01d. Nothing to promote. - No cloud-side override of these viewsets or managers exists — checked
unstract-cloud.
Open questions
/execution/<id>/files/— fix here, or land as a linked follow-up? Either is fine, but the PR body's security claim should be softened if it is deferred.WorkflowExecutionViewSet— same question. The commit message diagnoses the pattern precisely; was the sibling viewset checked and consciously deferred?- Was the missing
OWNERmembership row in_api_deploymentdeliberate, or an oversight? It changes what four of the six tests actually prove.
| return APIDeployment.objects.create( | ||
| api_name=f"api-{secrets.token_hex(4)}", | ||
| workflow=self.workflow, | ||
| organization=self.org, | ||
| created_by=self.owner, | ||
| shared_to_org=shared_to_org, | ||
| ) |
There was a problem hiding this comment.
[High] [Lens 13 — Testing] — The fixture builds an owner-less deployment, so the negative test asserts nothing
Under UN-2202 created_by is audit-only; creator access flows through an OWNER membership row. The base class does exactly that for self.workflow (tenant_account_v2/tests.py:98-99), but _api_deployment creates no membership row — and no post_save signal supplies one (tenant_account_v2/signals.py has only post_delete receivers; no ResourceMembership.objects.create exists outside tests and migrations).
So every deployment built here is visible to nobody, including self.owner. Two consequences:
test_unshared_deployment_stays_invisible(:95-97) would still pass ifQ(pk__in=member_ids)were deleted outright fromapi_v2/models.py:52. It reads as a negative control but constrains nothing.- The likeliest regression from this refactor — the owner can no longer see their own executions — has no test. The PR body claims coverage of "owner, co-owner, direct share, group share and
shared_to_org"; only the last two are actually asserted.
Suggested fix: add the OWNER membership row in _api_deployment, then add test_owner_sees_own_executions, a direct-viewer test via the base module's currently-unused _add_viewers (tenant_account_v2/tests.py:61-67), and a co-owner test. Re-check that test_unshared_deployment_stays_invisible still passes once the deployment is genuinely owned — that version is the one that tests something.
Confidence: High.
| def _execution(self, deployment: APIDeployment) -> WorkflowExecution: | ||
| return WorkflowExecution.objects.create( | ||
| workflow=self.workflow, | ||
| pipeline_id=deployment.id, | ||
| execution_mode=WorkflowExecution.Mode.INSTANT, | ||
| execution_method=WorkflowExecution.Method.DIRECT, | ||
| execution_type=WorkflowExecution.Type.COMPLETE, | ||
| status=ExecutionStatus.COMPLETED, | ||
| ) |
There was a problem hiding this comment.
[Medium] [Lens 13 — Testing] — Two of the three ORed branches in the rewritten for_user are never satisfied
_execution() hardcodes pipeline_id=deployment.id, and all six tests build their execution through it. Against models/execution.py:83:
final_filter = (workflow_filter & Q(pipeline_id__isnull=True)) | deployment_filterworkflow_filter & Q(pipeline_id__isnull=True)can never be true here, so the swap toWorkflow.objects.for_useratmodels/execution.py:69is completely unexercised — despite workflow-level executions being a primary flow, and despite being exactly the case in row 3 of the PR's manual test table.pipeline_filter(models/execution.py:75) is never satisfied — ETL/TASK pipeline executions have no coverage in either direction.
A regression hiding workflow-level executions from their owner, or leaking pipeline executions org-wide, ships green.
Suggested fix: parameterise _execution(deployment=None) so pipeline_id can be None; add (a) a group-shared self.workflow case asserting the member sees the workflow-level execution and the outsider does not, and (b) a Pipeline-backed twin of the existing grant/deny pair.
Confidence: High.
| def _log_queryset(self, user, execution_id): | ||
| """Run the log viewset's queryset build for ``user`` — the access gate.""" | ||
| request = APIRequestFactory().get("/") | ||
| request.user = user | ||
| view = WorkflowExecutionLogViewSet() | ||
| view.request = request | ||
| view.kwargs = {"pk": str(execution_id)} | ||
| return view.get_queryset() |
There was a problem hiding this comment.
[High] [Lens 13 — Testing] — No test reaches HTTP; the 403 and the entire export action are unverified
_log_queryset hand-constructs the viewset and calls get_queryset() directly, bypassing as_view(), initialize_request, initial(), check_permissions(), dispatch() and the DRF exception handler. Consequences:
- The PR's headline claim —
GET /execution/<id>/logs/returns 403 — has no automated assertion anywhere. The test provesPermissionDeniedis raised, not that the endpoint returns 403; that translation is entirely adispatch()-level behaviour the test never enters. - The
permission_classeschange from[IsAuthenticated, IsOwner]to[IsAuthenticated]is unpinned. exporthas zero coverage. It inherits the gate only incidentally, viaself.filter_queryset(self.get_queryset())atexecution_log_view.py:73, and nothing pins that coupling. A later streaming fast path, a.values()rewrite, an async hand-off, or hoisting the row-cap above the queryset build silently reopens bulk CSV/JSON exfiltration of any execution's logs by id — with CI green. This is the higher-impact of the two endpoints and the one with no test.
A future try/except around get_queryset() (a common pagination-hardening change) would downgrade the gate to an empty 200, and every test here still passes.
Suggested fix: route through as_view({"get": "list"}) and as_view({"get": "export"}) with force_authenticate, asserting 403 for the denied user and 200 for the shared user. The base module already establishes this idiom at tenant_account_v2/tests.py:344-348. Keep the existing unit-level tests as fast checks — they are fine, just not sufficient on their own for an authz change.
Confidence: High.
| class WorkflowExecutionLogViewSet(viewsets.ModelViewSet): | ||
| versioning_class = URLPathVersioning | ||
| permission_classes = [IsAuthenticated, IsOwner] | ||
| permission_classes = [IsAuthenticated] |
There was a problem hiding this comment.
[Medium] [Lens 2 — Architectural fit, Lens 4 — Security] — Dropping IsOwner leaves a ModelViewSet whose write handlers carry only IsAuthenticated
Removing IsOwner is correct — it implements only has_object_permission, which DRF never invokes on list/export, exactly as the commit message says. But the class is still a ModelViewSet (:30), so it retains create / update / partial_update / destroy.
Nothing is exploitable today because only list and export are routed (workflow_manager/execution/urls.py:15-16, workflow_v2/urls/workflow.py:23-24). The issue is the pattern: create never calls get_queryset(), so the new gate does not cover it, and the object-level class that used to sit in permission_classes is gone. "Drop the permission class, gate the queryset instead" is only safe on a read-only viewset, and nothing in the code says so — the next engineer who copies this onto a writable viewset inherits an unguarded write path.
Suggested fix: change the base to viewsets.ReadOnlyModelViewSet. One word, removes the latent write surface, and makes the queryset-gating pattern self-evidently sufficient. ExecutionLog is written only by workers and its fields are editable=False, so nothing legitimate is lost.
Confidence: High.
| - The workflow AND/OR | ||
| - The pipeline/API deployment |
There was a problem hiding this comment.
[Medium] [Lens 16 — Doc accuracy] — Docstring in this block claims org scoping comes from the view; it now comes only from this manager
Line 46 of this same docstring reads:
Service accounts see all executions (org-scoped by view).
That is false, and this diff makes it more dangerous:
- The service-account branch org-scopes itself at
:54-58; the view does not do it. - The only caller,
ExecutionViewSet, setsfilter_backends = [DjangoFilterBackend, DeterministicOrderingFilter](workflow_manager/execution/views/execution.py:37), which replacesDEFAULT_FILTER_BACKENDSand therefore dropsOrganizationFilterBackend— whose own docstring states "Viewsets MUST NOT override filter_backends" (utils/filters/organization_filter.py:31-33). - This diff relocates non-admin tenant isolation into the three delegated managers (see the new comment at
:66-68), makingfor_userthe sole tenant boundary for/execution/.
A maintainer trusting line 46 could remove the in-manager org filter and open a cross-tenant leak.
Suggested fix: rewrite line 46 to something like "Service accounts and org admins see all executions in the current organization; org scoping is enforced here, not by the view." Optionally, append to rather than replace filter_backends on ExecutionViewSet to restore the defence-in-depth.
For the record, the two comments this diff adds (:66-68 here and execution_log_view.py:42-44) are both accurate — I checked each claim against the code.
Confidence: High.
| # Defer to each resource's own ``for_user`` so execution visibility matches | ||
| # the resource list: memberships, group shares and ``shared_to_org`` | ||
| # (UN-2651). Those managers are org-scoped, so no explicit org arg. |
There was a problem hiding this comment.
[Medium] [Lens 13 — Testing] — No cross-organization test, on the change that makes shared_to_org load-bearing for execution visibility
This comment's claim — "Those managers are org-scoped, so no explicit org arg" — is correct today (DefaultOrganizationManagerMixin.get_queryset filters organization=UserContext.get_organization(), utils/models/organization_mixin.py:27-30). But nothing in the suite pins it.
WorkflowExecutionManager extends BaseModelManager (:30) and is not org-scoped. After this change, the only thing stopping Q(shared_to_org=True) inside APIDeployment.objects.for_user (api_v2/models.py:53) from matching another tenant's deployments is that mixin reading a thread-local. The suite runs with exactly one Organization, set once in GroupSharingTestBase.setUp. Dropping the mixin from any of the three sub-managers would pass every test in the new file.
This PR is precisely what makes shared_to_org load-bearing for execution visibility, so it is the right PR to pin it.
Suggested fix: one test — create a second Organization with a shared_to_org=True APIDeployment and an execution, assert self.outsider (org A) neither sees it via for_user nor can read its logs. The two-org fixture pattern already exists at tenant_account_v2/tests.py:207-216.
Confidence: Medium.
| workflow_filter = Q(workflow_id__in=Workflow.objects.for_user(user).values("pk")) | ||
|
|
||
| # Filter for API deployments the user can access | ||
| api_filter = Q( | ||
| pipeline_id__in=resources_visible_via_memberships(APIDeployment, user) | ||
| ) | ||
| api_filter = Q(pipeline_id__in=APIDeployment.objects.for_user(user).values("pk")) | ||
|
|
||
| # Filter for Pipelines the user can access | ||
| pipeline_filter = Q( | ||
| pipeline_id__in=resources_visible_via_memberships(Pipeline, user) | ||
| ) | ||
| pipeline_filter = Q(pipeline_id__in=Pipeline.objects.for_user(user).values("pk")) |
There was a problem hiding this comment.
[Medium] [Lens 9 — Performance] — for_user now costs three extra uncached admin lookups per call, on a polled path
for_user resolves is_user_organization_admin(user) at :60, then delegates to Workflow.objects.for_user, APIDeployment.objects.for_user and Pipeline.objects.for_user — each of which re-resolves the same predicate as its second statement.
OrganizationMemberService.is_user_organization_admin is uncached (tenant_account_v2/organization_member_service.py:31-45): one OrganizationMember.objects.get() round-trip plus an AuthenticationController() construction per call. Net +3 queries per for_user call for every non-admin user.
Where that lands:
/execution/(paginated list)./execution/<id>/logs/— which the Logs modal polls on a refresh interval, and which this PR newly routes throughfor_user.ExecutionViewSetcallsfor_usertwice on retrieve (workflow_manager/execution/views/execution.py:30and:44), so that path pays it six times.
Separately, each subquery went from a single indexed lookup on resource_membership to a SELECT DISTINCT over the resource table wrapping 2–3 nested INs. The subquery-not-materialised property that UN-2202 (#1797) deliberately tuned for this exact call site is preserved, so plan caching still holds — the added cost is depth, not an N+1.
Suggested fix: memoize the admin predicate. permissions/permission.py:57-67 already has exactly this cache, but keyed on request, which the managers never see — simplest is a per-user memo inside OrganizationMemberService.is_user_organization_admin.
Confidence: High on the mechanism, Medium on impact — an EXPLAIN on a realistic org plus the modal's poll interval would settle whether this is Medium or Low.
Standardized review — the 3 Low findingsSplit out from the review above so they don't compete with the High/Medium ones. None of these block anything. [Low] [Lens 3 — Correctness] —
The removed helper documented an explicit
The replacements go through That is fail-closed, so it is not a security issue, and I confirmed every current caller is a request-context viewset ( Suggested fix: narrow the comment to something like "org-scoped via Confidence: Medium — would rise to High if a worker or management path is later shown to call [Low] [Lens 13 — Testing] —
The property being claimed is that an unknown id and an inaccessible id are indistinguishable to an attacker probing for valid ids. The test asserts only that There is also a real distinction at the HTTP layer that this test structurally cannot see: Suggested fix: drive both cases through Confidence: High. [Low] [Lens 13 — Testing] — Randomised fixture value makes a failure non-reproducible from CI output
api_name=f"api-{secrets.token_hex(4)}"Only uniqueness is required, and per-test rollback makes collisions a non-issue — so this is not a flake source. But nothing seeds or logs the value, so a failure whose cause depended on the name could not be reproduced from the CI output. Suggested fix: derive the name from the test method name or a per-instance counter. Equivalent uniqueness, reproducible. Confidence: High. |



What
Why
How
for_user, which covers owner, co-owner, direct share, group share andshared_to_org./executions/<id>/logs/and/logs/export/gate on the executions the caller can see; an unknown id is denied the same way as an inaccessible one, so responses do not reveal which ids exist.IsOwnerfrom the log viewset — it implements onlyhas_object_permission, which DRF never calls on list/export.Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)
Database Migrations
Env Config
Relevant Docs
Related Issues or PRs
Dependencies Versions
Notes on Testing
Automated
backend/workflow_manager/execution/tests/test_shared_execution_access.py— 6 / 6 pass (4 of them fail onmain).test_group_share_exposes_the_deployment_executionstest_org_wide_share_exposes_the_deployment_executionsshared_to_orguser sees the executionstest_unshared_deployment_stays_invisibletest_logs_readable_once_the_deployment_is_sharedtest_logs_denied_when_the_execution_is_not_accessibletest_logs_denied_when_the_execution_is_unknownManual
Two accounts in one organisation: an owner/admin and a second user who is a member of a group.
Part 1 — the UI (visibility)
Part 2 — the API (access control)
The UI never links to a resource you can't see, so the gate itself can only be exercised by calling the endpoint directly.
GET /execution/<execution_id>/logs/with the workflow unsharedYou do not have access to logs for this execution.Worth knowing: access is granted through the workflow or the API deployment or the pipeline. Unsharing only the workflow still returns 200, correctly — every path has to be revoked before the 403 appears.
This is added in screenshot below
Screenshots:
Checklist
I have read and understood the Contribution Guidelines.