Skip to content

feat(oauth): block impersonated oauth when org disabled ai processing#60942

Merged
rafaeelaudibert merged 3 commits into
masterfrom
posthog-code/block-oauth-impersonation-ai-disabled
Jun 1, 2026
Merged

feat(oauth): block impersonated oauth when org disabled ai processing#60942
rafaeelaudibert merged 3 commits into
masterfrom
posthog-code/block-oauth-impersonation-ai-disabled

Conversation

@rafaeelaudibert
Copy link
Copy Markdown
Member

Problem

We allow staff to connect an MCP (and other OAuth clients) on behalf of a customer while impersonating their account — this is intended and useful. However, some organizations explicitly opt out of AI processing of their data via the org-level Organization.is_ai_data_processing_approved setting. In that case, a staff member impersonating the account should not be able to authorize an OAuth client that would route that organization's data into AI.

This restriction applies only to impersonation. Customers authorizing clients themselves are unaffected — they have already consented for their own data and know whether they can connect it.

Changes

  • New helper _impersonation_ai_processing_block(...) in posthog/api/oauth/views.py returns a 403 access_denied response when the session is an impersonation session and any in-scope organization is not approved for AI processing.
  • New helper _scoped_organization_ids(...) resolves the organizations a grant would actually reach: organization-level scope → the listed orgs, team-level scope → the orgs owning those teams, and all/unscoped → every org the impersonated user belongs to.
  • The guard is wired into all three token-minting paths of OAuthAuthorizationView: the first-party GET shortcut, the auto-approval GET path, and the consent POST path (only when allow=True).
  • Follows the codebase's fail-closed convention for AI features: only an explicit True counts as approved — a NULL/unset value is treated as not approved.

How did you test this code?

I'm an agent (PostHog Code). I added automated tests in posthog/test/test_oauth_impersonation.py (TestImpersonationAIProcessingBlock) covering:

  • no block when not impersonating, even if the org disabled AI processing
  • block when impersonating and the org is explicitly disabled (False) or unset (None)
  • no block when impersonating and the org is approved (True)
  • no block when a disabled org is out of the requested scope (precise per-scope check)
  • block when a scoped team belongs to a disabled org

I could not execute the test suite — the working environment has no Python/Django runtime or flox/hogli available. I verified both modified files compile (python3 -m py_compile), but the tests themselves have not been run. They should be run in a full dev environment before merge.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

🤖 Agent context

Authored by PostHog Code (Claude Opus 4.8).

Key decisions:

  • Enforcement points: the block is placed at the /oauth/authorize step (grant creation) rather than at /oauth/token code exchange — blocking authorize means no grant is created in the first place, which is the right gate. Pre-existing impersonation grants are already cleaned up by the logout-revocation signal.
  • Scope of the block: confirmed with the requester that the block should apply to all OAuth clients during impersonation (including first-party apps such as the toolbar and PostHog Code), not just external MCP clients. The error message is therefore client-agnostic. A more surgical option (exempting the non-AI toolbar) was considered and rejected in favor of the conservative behavior.
  • Approved semantics: treats NULL/unset as not approved to stay fail-closed and consistent with existing AI-feature gates (sync_vectors, summarization, embedding worker).

Organizations can opt out of AI processing of their data via
Organization.is_ai_data_processing_approved. When a staff member is
impersonating a customer, they must not be able to authorize an OAuth
client (the MCP being the motivating case) that would feed such an
organization's data into AI.

Adds a guard on all three token-minting paths of OAuthAuthorizationView
(first-party shortcut, auto-approval, and consent POST) that returns
403 access_denied during impersonation when any in-scope organization
is not approved for AI processing. Customers authorizing clients
themselves are unaffected. Mirrors the fail-closed convention used
elsewhere for AI features: only an explicit True counts as approved.

Generated-By: PostHog Code
Task-Id: 39394b5e-83bb-4693-96fb-52efb752a721
@graphite-app graphite-app Bot added the stamphog Request AI review from stamphog label Jun 1, 2026
Copy link
Copy Markdown

@stamphog stamphog Bot left a comment

Choose a reason for hiding this comment

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

This PR touches OAuth authorization and authentication flows, which are in the deny-list and require human review. Additionally, it has zero reviews despite modifying security-sensitive auth code that controls when impersonated OAuth grants are blocked.

@stamphog stamphog Bot removed the stamphog Request AI review from stamphog label Jun 1, 2026
@rafaeelaudibert rafaeelaudibert added the stamphog Request AI review from stamphog label Jun 1, 2026
@rafaeelaudibert rafaeelaudibert requested review from a team, MattBro and fercgomes and removed request for a team June 1, 2026 15:24
Copy link
Copy Markdown

@stamphog stamphog Bot left a comment

Choose a reason for hiding this comment

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

Gates denied this PR because it touches OAuth/auth code, which is on the deny-list and requires human review. The change modifies authorization flow logic in OAuth views.

@stamphog stamphog Bot removed the stamphog Request AI review from stamphog label Jun 1, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 1, 2026

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
posthog/test/test_oauth_impersonation.py:225
`_build_request` is annotated as returning `RequestFactory` but actually returns the request object produced by `RequestFactory().get("/")` (a `WSGIRequest`). The annotation is misleading and would fail a strict type check.

```suggestion
    def _build_request(self, target: User, *, impersonating: bool) -> WSGIRequest:
```

### Issue 2 of 2
posthog/api/oauth/views.py:692-695
**Auto-approval block ignores grant scope**

The auto-approval branch calls `_impersonation_ai_processing_block(request)` without `access_level`, `scoped_organization_ids`, or `scoped_team_ids`, so it always checks **all** of the impersonated user's organisations. The consent POST path is scope-aware (lines 750–756). In a multi-org scenario where the user belongs to an approved org and a disabled org, re-authorizing a token that is scoped exclusively to the approved org would be incorrectly blocked here, while the same grant via a fresh consent POST would succeed. If the existing token's scope information isn't easily recoverable at this point, the asymmetry is worth a comment explaining why the conservative fallback is intentional.

Reviews (1): Last reviewed commit: "feat(oauth): block impersonated oauth wh..." | Re-trigger Greptile

Comment on lines 692 to +695
for token in tokens:
if token.allow_scopes(scope_str.split()):
if block := _impersonation_ai_processing_block(request):
return block
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.

P2 Auto-approval block ignores grant scope

The auto-approval branch calls _impersonation_ai_processing_block(request) without access_level, scoped_organization_ids, or scoped_team_ids, so it always checks all of the impersonated user's organisations. The consent POST path is scope-aware (lines 750–756). In a multi-org scenario where the user belongs to an approved org and a disabled org, re-authorizing a token that is scoped exclusively to the approved org would be incorrectly blocked here, while the same grant via a fresh consent POST would succeed. If the existing token's scope information isn't easily recoverable at this point, the asymmetry is worth a comment explaining why the conservative fallback is intentional.

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/api/oauth/views.py
Line: 692-695

Comment:
**Auto-approval block ignores grant scope**

The auto-approval branch calls `_impersonation_ai_processing_block(request)` without `access_level`, `scoped_organization_ids`, or `scoped_team_ids`, so it always checks **all** of the impersonated user's organisations. The consent POST path is scope-aware (lines 750–756). In a multi-org scenario where the user belongs to an approved org and a disabled org, re-authorizing a token that is scoped exclusively to the approved org would be incorrectly blocked here, while the same grant via a fresh consent POST would succeed. If the existing token's scope information isn't easily recoverable at this point, the asymmetry is worth a comment explaining why the conservative fallback is intentional.

How can I resolve this? If you propose a fix, please make it concise.

…llback

Fixes ty type-check failure: `_build_request` is annotated as returning
the request object (`HttpRequest`), not `RequestFactory`.

Also documents why the auto-approval path uses the conservative
all-orgs check rather than the matched token's scope (per Greptile
review feedback).

Generated-By: PostHog Code
Task-Id: 39394b5e-83bb-4693-96fb-52efb752a721
@tests-posthog
Copy link
Copy Markdown
Contributor

tests-posthog Bot commented Jun 1, 2026

⏭️ Skipped snapshot commit because branch advanced to 16fb8e2 while workflow was testing 823d220.

The new commit will trigger its own snapshot update workflow.

If you expected this workflow to succeed: This can happen due to concurrent commits. To get a fresh workflow run, either:

  • Merge master into your branch, or
  • Push an empty commit: git commit --allow-empty -m 'trigger CI' && git push

@edwinyjlim
Copy link
Copy Markdown
Member

Should this strip the AI-related scopes like llm_gateway from the token instead of blocking the whole authorization?

As written, this 403s every OAuth client during impersonation on an opted-out org, not just MCP. Maybe that's intended tho, just thinking out loud

Copy link
Copy Markdown
Member Author

@edwinyjlim

Should this strip the AI-related scopes like llm_gateway from the token instead of blocking the whole authorization?

No, sending their data to Anthropic via our Claude Code instance shouldn't be allowed

As written, this 403s every OAuth client during impersonation on an opted-out org, not just MCP. Maybe that's intended tho, just thinking out loud

There's no way to detect it's an MCP vs. just any random OAuth flow. There are some clients that say who they are, but some like Cursor don't!

Copy link
Copy Markdown
Contributor

@fercgomes fercgomes left a comment

Choose a reason for hiding this comment

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

looks good

@rafaeelaudibert rafaeelaudibert enabled auto-merge (squash) June 1, 2026 16:59
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

🎭 Playwright report · View test results →

⚠️ 1 flaky test:

  • Save an insight, make changes, discard them, and save a copy (chromium)

These issues are not necessarily caused by your changes.
Annoyed by this comment? Help fix flakies and failures and it'll disappear!

@rafaeelaudibert rafaeelaudibert merged commit 61d9324 into master Jun 1, 2026
264 checks passed
@rafaeelaudibert rafaeelaudibert deleted the posthog-code/block-oauth-impersonation-ai-disabled branch June 1, 2026 17:18
@deployment-status-posthog
Copy link
Copy Markdown

deployment-status-posthog Bot commented Jun 1, 2026

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-06-01 17:43 UTC Run
prod-us ✅ Deployed 2026-06-01 18:00 UTC Run
prod-eu ✅ Deployed 2026-06-01 18:06 UTC Run

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.

3 participants