Skip to content

UN-2651 [FIX] Show execution logs to group and org-shared users - #2234

Open
kirtimanmishrazipstack wants to merge 3 commits into
mainfrom
UN-2651-shared-project-logs
Open

UN-2651 [FIX] Show execution logs to group and org-shared users#2234
kirtimanmishrazipstack wants to merge 3 commits into
mainfrom
UN-2651-shared-project-logs

Conversation

@kirtimanmishrazipstack

@kirtimanmishrazipstack kirtimanmishrazipstack commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What

  • Teammates who get a deployment or pipeline shared with them through a group, or shared with the whole organisation, can now see its runs on the Logs page. That page used to come up empty for them.
  • A run's logs can now only be opened by people the deployment or pipeline was actually shared with.

Why

  • A shared deployment whose Logs page is empty reads as broken, and it let down exactly the teammates sharing was meant to help. Sharing with one person already worked; sharing through a group or with the whole org did not.
  • Anyone in the organisation could read a run's logs if they had its link, even when nothing had been shared with them.

How

  • Executions resolve visibility through each resource's own for_user, which covers owner, co-owner, direct share, group share and shared_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.
  • Dropped IsOwner from the log viewset — it implements only has_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)

  • Only the logs endpoint narrows: a user who could previously fetch any execution's logs by id now needs access to that execution. No UI path exposed those ids, so normal flows are unaffected. The other change only widens visibility.

Database Migrations

  • None

Env Config

  • None

Relevant Docs

Related Issues or PRs

Dependencies Versions

  • None

Notes on Testing

Automated

backend/workflow_manager/execution/tests/test_shared_execution_access.py6 / 6 pass (4 of them fail on main).

Test Asserts
test_group_share_exposes_the_deployment_executions Group-shared user sees the executions
test_org_wide_share_exposes_the_deployment_executions shared_to_org user sees the executions
test_unshared_deployment_stays_invisible User with no share sees nothing
test_logs_readable_once_the_deployment_is_shared Shared user reads the logs
test_logs_denied_when_the_execution_is_not_accessible Unshared user is denied
test_logs_denied_when_the_execution_is_unknown An id that doesn't exist is denied the same way, so responses can't be used to probe which ids exist

Manual

Two accounts in one organisation: an owner/admin and a second user who is a member of a group.

Part 1 — the UI (visibility)

# Who Action Result
1 Owner Opens the Logs page for a workflow run Logs listed
2 Owner Exports the run's logs CSV and JSON both download
3 Second user Workflow is group-shared; opens the Logs page Sees the same logs — this is the fix
4 Owner / admin Unshares the workflow from the group (removing the user from the group does the same) Workflow disappears from the second user's UI, and its logs with it

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.

# Who Action Result
5 Second user GET /execution/<execution_id>/logs/ with the workflow unshared 403You 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:

4

Checklist

I have read and understood the Contribution Guidelines.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Bug Fixes
    • Improved access controls for workflow execution logs.
    • Prevented unauthorized users from viewing logs or discovering restricted executions.
    • Ensured organization and group sharing permissions are consistently respected across executions, workflows, deployments, and pipelines.
    • Added coverage for shared, restricted, and inaccessible execution scenarios.

Walkthrough

The 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.

Changes

Execution access control

Layer / File(s) Summary
User-scoped execution visibility
backend/workflow_manager/workflow_v2/models/execution.py
WorkflowExecutionManager.for_user now resolves workflow, API deployment, and pipeline visibility through their for_user querysets.
Execution log authorization
backend/workflow_manager/workflow_v2/execution_log_view.py
The log view uses WorkflowExecution.objects.for_user(...) and raises PermissionDenied for inaccessible or unknown executions before querying logs.
Shared access validation
backend/workflow_manager/execution/tests/test_shared_execution_access.py
Tests cover organization-wide sharing, group sharing, unshared deployments, denied log access, and log access after deployment sharing.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: enabling execution log visibility for group- and organization-shared users.
Description check ✅ Passed The description covers the required change, rationale, implementation, risks, migrations, configuration, testing, and checklist items.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch UN-2651-shared-project-logs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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>
@kirtimanmishrazipstack
kirtimanmishrazipstack force-pushed the UN-2651-shared-project-logs branch from f680ac4 to 5faa618 Compare August 7, 2026 17:54
@kirtimanmishrazipstack kirtimanmishrazipstack changed the title UN-2651 [FIX] Show execution logs to group-shared and org-shared users UN-2651 [FIX] Show execution logs to group and org-shared users Aug 7, 2026
@kirtimanmishrazipstack
kirtimanmishrazipstack marked this pull request as ready for review August 10, 2026 06:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6b916ea and 6c5f8cd.

📒 Files selected for processing (3)
  • backend/workflow_manager/execution/tests/test_shared_execution_access.py
  • backend/workflow_manager/workflow_v2/execution_log_view.py
  • backend/workflow_manager/workflow_v2/models/execution.py

@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR aligns execution and log visibility with the sharing rules of the associated workflow, pipeline, or API deployment.

  • Delegates execution visibility to each resource’s sharing-aware for_user manager.
  • Authorizes log listing and export against the caller-visible execution queryset.
  • Adds coverage for group sharing, organization-wide sharing, inaccessible executions, and unknown execution IDs.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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]
Loading

Reviews (2): Last reviewed commit: "UN-2651 [FIX] Test that unknown executio..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
backend/workflow_manager/workflow_v2/execution_log_view.py (1)

32-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve 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 ClassVar or 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6b916ea and 6c5f8cd.

📒 Files selected for processing (3)
  • backend/workflow_manager/execution/tests/test_shared_execution_access.py
  • backend/workflow_manager/workflow_v2/execution_log_view.py
  • backend/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>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 16.8
e2e-coowners e2e 1 0 0 0 1.7
e2e-etl e2e 1 0 0 0 4.1
e2e-login e2e 2 0 0 0 1.3
e2e-prompt-studio e2e 1 0 0 0 4.8
e2e-smoke e2e 2 0 0 0 1.0
e2e-workflow e2e 1 0 0 0 16.7
integration-backend integration 273 0 0 26 46.5
integration-connectors integration 1 0 0 7 8.1
integration-workers integration 140 0 0 1 49.3
unit-backend unit 998 0 0 1 32.5
unit-connectors unit 63 0 0 0 13.2
unit-core unit 33 0 0 0 1.0
unit-platform-service unit 15 0 0 0 2.7
unit-rig unit 117 0 0 0 6.0
unit-sdk1 unit 480 0 0 0 21.8
unit-workers unit 1335 0 0 1 93.7
TOTAL 3466 0 0 36 321.1

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@chandrasekharan-zipstack chandrasekharan-zipstack left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-24

No 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-83workflow_manager/urls.py:20backend/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 IsOwner removal 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

  1. /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.
  2. WorkflowExecutionViewSet — same question. The commit message diagnoses the pattern precisely; was the sibling viewset checked and consciously deferred?
  3. Was the missing OWNER membership row in _api_deployment deliberate, or an oversight? It changes what four of the six tests actually prove.

Comment on lines +42 to +48
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,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:

  1. test_unshared_deployment_stays_invisible (:95-97) would still pass if Q(pk__in=member_ids) were deleted outright from api_v2/models.py:52. It reads as a negative control but constrains nothing.
  2. 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.

Comment on lines +50 to +58
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,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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_filter
  • workflow_filter & Q(pipeline_id__isnull=True) can never be true here, so the swap to Workflow.objects.for_user at models/execution.py:69 is 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.

Comment on lines +73 to +80
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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 proves PermissionDenied is raised, not that the endpoint returns 403; that translation is entirely a dispatch()-level behaviour the test never enters.
  • The permission_classes change from [IsAuthenticated, IsOwner] to [IsAuthenticated] is unpinned.
  • export has zero coverage. It inherits the gate only incidentally, via self.filter_queryset(self.get_queryset()) at execution_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]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment on lines +37 to +38
- The workflow AND/OR
- The pipeline/API deployment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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, sets filter_backends = [DjangoFilterBackend, DeterministicOrderingFilter] (workflow_manager/execution/views/execution.py:37), which replaces DEFAULT_FILTER_BACKENDS and therefore drops OrganizationFilterBackend — 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), making for_user the 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.

Comment on lines +66 to +68
# 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Comment on lines +69 to +75
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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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_usereach 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 through for_user.
  • ExecutionViewSet calls for_user twice on retrieve (workflow_manager/execution/views/execution.py:30 and :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.

@chandrasekharan-zipstack

Copy link
Copy Markdown
Contributor

Standardized review — the 3 Low findings

Split out from the review above so they don't compete with the High/Medium ones. None of these block anything.


[Low] [Lens 3 — Correctness] — for_user now returns empty rather than membership-scoped results outside request context

backend/workflow_manager/workflow_v2/models/execution.py:66-78

The removed helper documented an explicit organization argument for exactly this case:

Falls back to UserContext so request paths need no change; pass organization explicitly on worker/management paths where UserContext is empty.
tenant_account_v2/sharing_helpers.py:208-213

The replacements go through DefaultOrganizationManagerMixin.get_queryset, which does filter(organization=UserContext.get_organization()) (utils/models/organization_mixin.py:27-30). With no org context that becomes organization_id IS NULL, so a non-admin for_user call from Celery or a management command silently returns nothing.

That is fail-closed, so it is not a security issue, and I confirmed every current caller is a request-context viewset (workflow_manager/execution/views/execution.py:30,44 and execution_log_view.py:46). But the escape hatch the old helper deliberately provided is gone, and the new comment at :68 — "Those managers are org-scoped, so no explicit org arg" — reads as unconditional when it really means "correct inside a request".

Suggested fix: narrow the comment to something like "org-scoped via UserContext; request paths only", so a future worker-side caller is warned rather than surprised.

Confidence: Medium — would rise to High if a worker or management path is later shown to call WorkflowExecution.objects.for_user.


[Low] [Lens 13 — Testing] — test_logs_denied_when_the_execution_is_unknown cannot fail independently

backend/workflow_manager/execution/tests/test_shared_execution_access.py:104-108

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 PermissionDenied is raised — from the single raise at execution_log_view.py:50 that both cases reach identically. It is true by construction and can only fail in lockstep with its sibling at :99-102. It never compares status code, body, or headers, which are what an enumerator actually observes.

There is also a real distinction at the HTTP layer that this test structurally cannot see: execution/urls.py:22 uses <uuid:pk>, so a malformed id 404s from URL resolution while an unknown-but-valid UUID 403s.

Suggested fix: drive both cases through as_view({"get": "list"}) and assert the two responses have equal status_code and equal .data. That is the actual property, and it can then fail on its own.

Confidence: High.


[Low] [Lens 13 — Testing] — Randomised fixture value makes a failure non-reproducible from CI output

backend/workflow_manager/execution/tests/test_shared_execution_access.py:43

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants