Skip to content

fix(livestream): recheck current project membership - #108135

Open
pauldambra wants to merge 7 commits into
masterfrom
posthog/fix-live-stream-membership
Open

pauldambra wants to merge 7 commits into
masterfrom
posthog/fix-live-stream-membership

Conversation

@pauldambra

@pauldambra pauldambra commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

[Robot] Prepared by PostHog Desktop.

Problem

Live streams need to apply current project access when organization or project membership changes.
This is the separate service change from the review of #99248.

Changes

  • Check current access before opening event streams, notifications, or stats requests.
  • Check open event streams every 30 seconds and notifications every 15 seconds.
  • Close streams when access ends or the authorization service is unavailable.
  • Use the existing token and project access rules. The API also checks account status, organization status, and project token rotation.
  • Keep event and notification delivery active while periodic access checks run.

Before:

flowchart LR
    Client[Client] --> JWT[Token check] --> Stream[Live stream]
    classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000;
    classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff;
    class Client,Stream phYellow;
    class JWT phBlue;
Loading

After configuration:

flowchart LR
    Client[Client] --> JWT[Token check] --> API[Current access check] --> Stream[Live stream]
    Stream -->|Periodic check| API
    classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000;
    classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff;
    classDef phRed fill:#f54e00,stroke:#f54e00,color:#fff;
    class Client,Stream phYellow;
    class JWT phBlue;
    class API phRed;
Loading

How did you test this code?

  • Python API tests cover revoked access, blocked organizations, rotated tokens, missing credentials, and wrong token audiences.
  • Go tests cover authorization failures, redirect rejection, connection failure, and the configuration default.
  • Fake-time Go tests cover event and notification delivery during access checks, access denial, and cancellation; the handlers also pass race detection.
  • Full mypy, Ruff, security rules, and OpenAPI generation completed locally or on an isolated devbox.
  • OpenAPI generation produced no generated-file changes.
  • Not checked: a deployed stream service or production API load.

Release status

  • No feature flag controls this change

Note

The new check is inactive until LIVESTREAM_JWT_AUTHORIZATION_URL points to the regional API /api/livestream/authorize/ endpoint.
Deploy the API first. Configure one stream service instance, then check authorization failures and stream connections before expanding the rollout.
API failures close streams. Watch reconnect rates and API load during the staged rollout.
Existing tokens must include user and organization claims. Leave the setting empty to preserve current behavior during deployment.

Automatic notifications

  • Publish to changelog?

Docs update

No existing document under docs/ covers this service setting. The rollout steps are above.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: PostHog Desktop / Codex, GPT-6; gpt-5.6-sol wrote the review fixes. gpt-6-astra validated the authorization and concurrency changes.

Tools: Git, GitHub CLI, hogli, pytest, Go, mypy, Ruff, Semgrep, and CodeRabbit.
Skills: improving-drf-endpoints, writing-tests, writing-code-comments, writing-user-facing-copy, setting-up-devbox, running-ci-preflight, reviewing-with-coderabbit, writing-pr-descriptions, pr-shepherd, qa-swarm, review-triage, paul-pair, simplify, ci-shepherd, and security-audit.
The open PR search found no separate membership fix. Test data is invented from the public code contract.
The first CodeRabbit review found no issues. The combined stack review raised outage handling and the optional rollout setting. Both are intentional: authorization failures stop access, and the setting permits API-first deployment.


Created with PostHog Desktop

Generated-By: PostHog Desktop
Task-Id: 160dfc49-dceb-4887-b9f5-a93b2da017ed
Generated-By: PostHog Desktop
Task-Id: 160dfc49-dceb-4887-b9f5-a93b2da017ed
Generated-By: PostHog Desktop
Task-Id: 160dfc49-dceb-4887-b9f5-a93b2da017ed
@pauldambra pauldambra self-assigned this Sep 29, 2026
@trunk-io

trunk-io Bot commented Sep 29, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@posthog

posthog Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

FLASH MODE - Faster, but stupid, use regular ReviewHog for a heavy review

🦔 PostHog Review reviewed this pull request

Nothing worth raising this time. Enjoy the moment:

A happy dog on a sunny path

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Playwright — all passed

All tests passed.

View test results →

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

📝 Walkthrough

Walkthrough

The change adds a livestream authorization endpoint that validates tokens and current access. The Go service calls the endpoint before handling stats, events, and notifications requests. It also rechecks access during open event and notification streams and closes them when access fails.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 0a06d

Revoked users can receive a queued notification or event after an access check denies them. Close both delivery gaps before merging; the authorization endpoint’s transport configuration also needs owner attention.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 0a06d

The new checks improve revocation handling, but they are disabled when the authorization URL is unset. A stream can also deliver a queued item after a periodic check denies access. Whether the URL is configured and reachable in production remains unverified.

Retained concerns

  • High · security · inferred: Current-membership enforcement depends on a deployment-supplied URL. If it is unset, every new check succeeds without consulting PostHog, leaving token-only access in that deployment.
  • Medium · security · observed: The new periodic-denial transition does not stop delivery atomically: a queued notification can be written after the authorization error is ready. This limits the new fail-close guarantee, although periodic checks improve on the former absence of rechecks.
Security review details

Security Blast Radius

  • inferred — An unset URL affects current-access checks for stats, events, and notifications throughout a Livestream deployment. The production configuration, and thus effective exposure, is unknown.

Security Findings and Attack Paths

  • observed — The retained finding concerns a revoked stream with a queued, matching notification: when the denial and message are both ready, delivery may win the selection. The handler does eventually return on denial; the PR adds rechecks where none existed before.
  • inferred — The event handler likewise selects a queued event alongside a periodic authorization error, so its new denial transition has the same ordering limitation. This does not establish broader access than the former stream, which had no periodic recheck.

Trust Boundaries and Controls

  • observed — A client-supplied bearer token crosses from Livestream to PostHog, where identity and current permissions are evaluated. In Livestream, a configured 401 or 403 denies access and transport failure fails closed.

Resilience and Maintainability Implications

  • observed — Notification subscription cancellation and event unsubscription occur when their handlers return, but neither cleanup path prevents a queued delivery case from winning before return.

Hardening Proposals

  • proposed — Make activation of the authorization URL explicit and verifiable for each deployment before treating current membership as an enforced control.
  • proposed — Coordinate periodic denial with payload writes so queued notifications and events cannot be emitted after the handler has received an access failure.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and matches the required template. It explains the problem, user-visible changes, flow changes, testing coverage, release status, rollout conditions, documentation status, …
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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

@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

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
livestream/configs/configs.go-134-134 (1)

134-134: 🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🟡 Minor | ⚡ Quick win

Security Misconfiguration

Reachability: Internal
Exploitability: Difficult
CWE: CWE-319 — Cleartext Transmission of Sensitive Information

Reject non-loopback HTTP authorization URLs.

CheckAccess forwards the incoming bearer token to jwt.authorization_url on the initial request and every 30-second stream check. A plain-HTTP URL sends the token without transport encryption. Require HTTPS, allow HTTP only for loopback URLs used in tests, and document the environment variable.

Validate the authorization URL in LoadConfig
 import (
 	"errors"
 	"log"
+	"net/url"
 	"strings"
 
 	"github.com/spf13/viper"
 )
@@
 	_ = viper.BindEnv("jwt.secret")           // LIVESTREAM_JWT_SECRET
 	_ = viper.BindEnv("jwt.secret_fallbacks") // LIVESTREAM_JWT_SECRET_FALLBACKS (comma-separated)
-	_ = viper.BindEnv("jwt.authorization_url")
+	_ = viper.BindEnv("jwt.authorization_url") // LIVESTREAM_JWT_AUTHORIZATION_URL
@@
 	if err := viper.Unmarshal(&config); err != nil {
 		return nil, err
 	}
 
+	if config.JWT.AuthorizationURL != "" {
+		u, err := url.Parse(config.JWT.AuthorizationURL)
+		if err != nil || u.Hostname() == "" ||
+			(!strings.EqualFold(u.Scheme, "https") &&
+				!(strings.EqualFold(u.Scheme, "http") &&
+					(strings.EqualFold(u.Hostname(), "localhost") || u.Hostname() == "127.0.0.1" || u.Hostname() == "::1"))) {
+			return nil, errors.New("jwt.authorization_url must use HTTPS or loopback HTTP")
+		}
+	}
+
 	// Set default values

Source: Learnings


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: ac60a18d-84bc-4492-9110-edac905f76e8

📥 Commits

Reviewing files that changed from the base of the PR and between bf9b9f2 and 93afc13.

📒 Files selected for processing (8)
  • livestream/auth/access.go
  • livestream/auth/access_test.go
  • livestream/configs/configs.go
  • livestream/handlers/handlers.go
  • livestream/handlers/handlers_test.go
  • posthog/api/livestream.py
  • posthog/api/test/test_livestream.py
  • posthog/urls.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.

Comment thread livestream/auth/access.go Outdated
@posthog

posthog Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

FLASH MODE - Faster, but stupid, use regular ReviewHog for a heavy review
PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog 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.

FLASH MODE - Faster, but stupid, use regular ReviewHog for a heavy review

PostHog Review

Found 1 should fix.

Comment thread posthog/api/livestream.py
permission_classes = [IsAuthenticated]

def get(self, request: Request) -> Response:
level = UserPermissions(cast(User, request.user)).team(cast(Team, request.auth)).effective_membership_level

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.

FLASH MODE - Faster, but stupid, use regular ReviewHog for a heavy review

Permission checks load access rules for every project

should_fix performance

Issue description

For each check, effective_membership_level loads _prefetched_access_controls. That query materializes project access-control rows for every team in all of the user's organizations, even though this endpoint checks one team. With checks running every 15 or 30 seconds per stream, this can add substantial database and memory work as organizations grow.

Why we think it's a valid issue
  • Checked: Traced LivestreamAuthorizationView.get through UserPermissions.team(...).effective_membership_level and checked when access-control data is loaded and reused.
  • Found: When the organization has access control enabled, effective_membership_level_for_parent_membership reads _prefetched_access_controls before filtering rules for the requested team (posthog/user_permissions.py:207-215). That property queries project access-control rows across all of the user’s organizations and builds an in-memory dictionary of every returned row (posthog/user_permissions.py:113-134). The endpoint creates a new UserPermissions for each authorization request (posthog/api/livestream.py:51), while livestream checks recur every 15 or 30 seconds (livestream/handlers/handlers.go:172-177, livestream/handlers/handlers.go:323-352).
  • Impact: Each check can query and materialize unrelated project rules, and the work repeats across active streams. The cost grows with access-control rows in the user’s organizations, so this is a concrete performance concern.
Suggested fix

Use a permission check that filters access-control rows to the requested team, or add a single-team path so periodic checks do not load rules for unrelated projects.

Prompt to fix with AI (copy-paste)
## Context
@posthog/api/livestream.py#L51

<issue_description>
For each check, `effective_membership_level` loads `_prefetched_access_controls`. That query materializes project access-control rows for every team in all of the user's organizations, even though this endpoint checks one team. With checks running every 15 or 30 seconds per stream, this can add substantial database and memory work as organizations grow.
</issue_description>

<issue_validation>
- **Checked:** Traced `LivestreamAuthorizationView.get` through `UserPermissions.team(...).effective_membership_level` and checked when access-control data is loaded and reused.
- **Found:** When the organization has access control enabled, `effective_membership_level_for_parent_membership` reads `_prefetched_access_controls` before filtering rules for the requested team (`posthog/user_permissions.py:207-215`). That property queries project access-control rows across all of the user’s organizations and builds an in-memory dictionary of every returned row (`posthog/user_permissions.py:113-134`). The endpoint creates a new `UserPermissions` for each authorization request (`posthog/api/livestream.py:51`), while livestream checks recur every 15 or 30 seconds (`livestream/handlers/handlers.go:172-177`, `livestream/handlers/handlers.go:323-352`).
- **Impact:** Each check can query and materialize unrelated project rules, and the work repeats across active streams. The cost grows with access-control rows in the user’s organizations, so this is a concrete performance concern.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Use a permission check that filters access-control rows to the requested team, or add a single-team path so periodic checks do not load rules for unrelated projects.
</potential_solution>

Generated-By: PostHog Desktop
Task-Id: 160dfc49-dceb-4887-b9f5-a93b2da017ed
@trunk-io

trunk-io Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

@pauldambra
pauldambra added this pull request to stack #108171 September 29, 2026 08:42
@pauldambra
pauldambra marked this pull request as ready for review September 29, 2026 08:58
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T09:07:13.896861Z f696fcc Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team September 29, 2026 08:59

@pauldambra pauldambra left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Robot]

Note

🤖 Automated comment by QA Swarm — not written by a human

QA review found one event delivery risk. See the inline comment.

Comment thread livestream/handlers/handlers.go Outdated
@pauldambra

pauldambra commented Sep 29, 2026 •

Copy link
Copy Markdown
Member Author

[Robot]

Note

🤖 Automated comment by QA Swarm — not written by a human

Verdict: APPROVE WITH NITS (round 3 @ 0a06d4c)

The final review found no new code issue. Two earlier performance questions remain open for a separate decision.

Key findings

  • Fixed: periodic checks no longer block event or notification delivery. Denial closes the stream and cancels its workers.
  • Fixed: the authorization endpoint rejects inactive organizations and organizations pending deletion.
  • Deferred: narrow the shared permission query only after checking that access rules stay the same.
  • Deferred: assess stats request load during rollout before adding a cache that changes the revocation delay.

Convergence

Both reviewers approved the notification fix. The periodic checks do not promise an atomic cutoff at the instant the access service returns a denial.

Reviewer summaries

Reviewer Assessment
Router, gpt-5.6-sol No substantive findings in the final notification fix.
Security and reliability, gpt-6-astra Approved cancellation, response ownership, and periodic access checks.

Previous rounds

  • Round 1 @ f696fcc: found blocked event delivery; triage also fixed organization status checks.
  • Round 2 @ 34d0001: the event fix passed review; a bot review found the same blocking pattern in notifications.

Automated by QA Swarm — not a human review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f696fcc3b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread posthog/api/livestream.py
if err != nil {
return c.JSON(http.StatusUnauthorized, resp{Error: "wrong token claims"})
}
if err := auth.CheckAccess(c.Request().Context(), c.Request().Header); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Throttle authorization on the 1.5-second stats path

When the Live Events page is open, it polls /stats every 1,500 ms (LiveEventsTable.tsx:32,74), so this new call generates about 40 synchronous Django authorization requests per minute per viewer. Each request performs User and Team lookups and, for access-control organizations, UserPermissions loads every project access-control row across all organizations the user belongs to (posthog/user_permissions.py:113-134), despite checking one team. This multiplies organization-wide database scans by active viewers and can make the three-second fail-closed timeout disconnect users under load; cache or throttle the stats authorization decision to the 15–30-second recheck cadence, or use a team-scoped resolver.

Useful? React with 👍 / 👎.

Generated-By: PostHog Desktop
Task-Id: 160dfc49-dceb-4887-b9f5-a93b2da017ed

@posthog posthog 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.

FLASH MODE - Faster, but stupid, use regular ReviewHog for a heavy review

PostHog Review

Found 1 should fix.

Comment thread livestream/handlers/handlers.go Outdated
Generated-By: PostHog Desktop
Task-Id: 160dfc49-dceb-4887-b9f5-a93b2da017ed
Generated-By: PostHog Desktop
Task-Id: 160dfc49-dceb-4887-b9f5-a93b2da017ed

@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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Check access errors before writing queued events. · handlers.go:196-203

livestream/handlers/handlers.go:196-203
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Check access errors before writing queued events.

periodicAccessChecks sends a denial to a buffered channel and returns. If subscription.EventChan also contains an event, the stream loop can select the event case and call event.WriteTo before processing the denial. The revoked client can therefore receive an event after access is denied.

Suggested fix
 				event := Event{
 					Data: jsonData,
 				}
+				select {
+				case err := <-accessErrors:
+					log.Warnf("Live stream authorization check failed: %v", err)
+					return nil
+				default:
+				}
 				if err := event.WriteTo(w); err != nil {
 					return err
 				}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: ff82f0e7-d7cf-4e4b-bf54-52282d92134c

📥 Commits

Reviewing files that changed from the base of the PR and between df2f0e0 and 0a06d4c.

📒 Files selected for processing (2)
  • livestream/handlers/handlers.go
  • livestream/handlers/handlers_test.go

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 8 remain after this review.

Comment thread livestream/handlers/handlers.go
@pauldambra pauldambra added the stamphog Request AI approval (no full review) label Sep 29, 2026
@stamphog stamphog Bot removed the stamphog Request AI approval (no full review) label Sep 29, 2026

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not approved yet — waiting on the conditions below.

Re-add the stamphog label to request another review once you have addressed this.

Two gates refused this pull request, so it needs a human reviewer. The deny-list gate flagged it for matching auth, which covers the new livestream/auth/access.go and the authorization endpoint in posthog/api/livestream.py. The tier gate classified it as T2-never (568 lines across 8 files, spanning two areas: the Go livestream service and the Python API), and that tier is never handled automatically.

The author can ask a human reviewer, such as someone on the owning team for auth or livestream, to review it. Splitting it into a Python API change and a separate Go service change might make that review easier, but the auth match would still need human sign-off.

  • coderabbitai[bot] reviewed the current head.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✗ matches: auth
size ✓ 167L, 5F substantive, 568L/8F incl. docs/generated/snapshots — within ceiling
tier ✗ classified as T2-never: T2-never (568L, 8F, two-areas, fix)
stamphog 2.3.0 .stamphog/policy.yml @ unknown · reviewed head 0a06d4c

This branch has not been deployed

No deployments
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