Skip to content

Feature/add source wizard#1423

Open
himanshudube97 wants to merge 8 commits into
mainfrom
feature/add-source-wizard
Open

Feature/add source wizard#1423
himanshudube97 wants to merge 8 commits into
mainfrom
feature/add-source-wizard

Conversation

@himanshudube97

@himanshudube97 himanshudube97 commented Jul 10, 2026

Copy link
Copy Markdown
Member

Architecture — Google OAuth for Airbyte sources

Dalgo-driven flow (Variant A: Dalgo performs the token exchange, not Airbyte). One Google Cloud OAuth app is shared across all Google connectors; only scopes + the credentials block shape vary per connector.

Layers

api/airbyte_api.py            thin HTTP — permissions, schema validation, delegate
core/oauth/
  ├─ google_oauth_service.py  handshake: consent URL, state nonce, code exchange, ref
  └─ google_oauth_provider.py registry: def-id → { scopes, credentials_builder }
ddpairbyte/airbytehelpers.py  save_oauth_source → airbyte_service.create/update_source

Request flow (3 backend hits)

1. POST /sources/oauth/consent/          [JWT]
      mint state nonce (redis, 10m) → build Google consent URL → return authUrl
      browser opens Google consent in a popup

2. GET  /sources/oauth/callback          [NO JWT — the state nonce IS the auth]
      Google 302-redirects the popup here with ?code&state
      pop + validate state  →  exchange code with Google (server-side, uses secret)
      stash refresh_token in redis under a single-use `ref` (5m)
      302 the popup → frontend  ?ref=...     (popup postMessages ref to opener, closes)

3. POST /sources/oauth/create/           [JWT]
      redeem `ref` → refresh_token
      connector.credentials_builder(client_id, client_secret, refresh_token) → credentials
      airbyte_service.create_source / update_source   (update = re-auth)

Security

  • client_secret and refresh_token never reach the browser — only the opaque ref does.
  • state and ref are single-use nonces, consumed via an atomic get+delete (MULTI/EXEC pipeline — Redis <6.2 compatible), closing the replay window.
  • Callback has no JWT (a browser redirect carries none); the unguessable state nonce is both CSRF protection and identity.

Extensibility

Add a Google source (Drive, Analytics, Ads) = one registry entry in google_oauth_provider.py (def-id → scopes + a credentials builder). No changes to the service, API, or callback.


Summary by CodeRabbit

  • New Features

    • Added Google OAuth support for Airbyte source connections (including Google Sheets), with consent, OAuth callback handling, and source create/re-auth flows.
    • Implemented secure server-side token exchange and single-use refs for improved protection during the browser flow.
    • Added required environment configuration options for Google OAuth credentials and redirect URLs.
  • Tests

    • Added end-to-end coverage for happy paths, redirect error scenarios, ref/state expiration, re-authentication updates, and cross-workspace validation.

himanshudube97 and others added 4 commits July 9, 2026 02:22
…byte delegation

Move the Google Sheets OAuth token exchange out of Airbyte into Dalgo. Instead of
registering client id/secret with Airbyte instance-wide and letting it exchange the
code, Dalgo now builds the Google consent URL itself, receives Google's redirect on
its own backend callback, exchanges the code for a refresh_token server-side, and
stashes it in Redis under a single-use opaque `ref`.

- consent: POST /sources/oauth/consent -> {authUrl} (builds Google URL; state nonce)
- callback: GET /sources/oauth/callback (PUBLIC, state-authenticated) -> exchanges
  code, stashes refresh_token under `ref`, 302s to the frontend with only the ref
- create: POST /sources/oauth/create -> redeems `ref`, injects credentials
  (client_id/secret from env + refresh_token), creates/updates the source

The auth code, client_secret, and refresh_token never reach the browser (only `ref`).
Scope is spreadsheets.readonly (least privilege). Removes the Airbyte source_oauths/*
wrappers, the SourceOAuthComplete schema/endpoint, and the instance-wide params
management command. 11 OAuth tests + full airbyte module (50) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@himanshudube97 himanshudube97 self-assigned this Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds Google OAuth support for Airbyte sources, including connector configuration, consent and callback endpoints, Redis-backed state and refresh-token references, credential injection during source creation, and comprehensive flow tests.

Changes

Google OAuth Airbyte source flow

Layer / File(s) Summary
OAuth contracts and connector registry
.env.template, ddpui/ddpairbyte/schema.py, ddpui/core/oauth/google_oauth_provider.py
Adds OAuth environment variables, request schemas, Google Sheets connector registration, credential construction, and client credential validation.
OAuth state and token exchange
ddpui/core/oauth/google_oauth_service.py
Builds consent and callback URLs, stores single-use state and refs in Redis, exchanges authorization codes with Google, and redirects the frontend with a ref or error.
API routes and source persistence
ddpui/api/airbyte_api.py, ddpui/ddpairbyte/airbytehelpers.py
Adds consent, callback, and source creation endpoints; redeems OAuth refs and creates or updates Airbyte sources with generated credentials.
OAuth flow validation
ddpui/tests/api_tests/test_airbyte_api.py
Tests consent, callback success and failures, ref consumption, credential injection, expiration, cross-organization access, and source-definition validation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant AirbyteAPI
  participant OAuthService
  participant Redis
  participant Google
  participant Airbyte
  Browser->>AirbyteAPI: Start Google OAuth consent
  AirbyteAPI->>OAuthService: Build consent URL and state
  OAuthService->>Redis: Store state
  Browser->>Google: Authorize connector
  Google->>AirbyteAPI: Return callback code and state
  AirbyteAPI->>OAuthService: Exchange code and complete OAuth
  OAuthService->>Google: Exchange authorization code
  OAuthService->>Redis: Store single-use ref
  OAuthService-->>Browser: Redirect with ref
  Browser->>AirbyteAPI: Create or update source with ref
  AirbyteAPI->>OAuthService: Redeem ref
  AirbyteAPI->>Airbyte: Create or update OAuth source
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is broadly related to source setup, but it is too generic to convey the main Google OAuth/Airbyte change. Use a more specific title such as "Add Google OAuth flow for Airbyte sources" to reflect the primary change.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 feature/add-source-wizard

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.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.73684% with 8 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@f03abed). Learn more about missing BASE report.

Files with missing lines Patch % Lines
ddpui/core/oauth/google_oauth_service.py 95.55% 4 Missing ⚠️
ddpui/core/oauth/google_oauth_provider.py 89.65% 3 Missing ⚠️
ddpui/api/airbyte_api.py 94.73% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1423   +/-   ##
=======================================
  Coverage        ?   62.17%           
=======================================
  Files           ?      154           
  Lines           ?    17968           
  Branches        ?        0           
=======================================
  Hits            ?    11171           
  Misses          ?     6797           
  Partials        ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Actionable comments posted: 3

🤖 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 `@ddpui/core/oauth/google_oauth_service.py`:
- Around line 144-159: Update exchange_google_oauth_code to catch requests.post
timeouts and response.json parsing failures, convert both into HttpError using
the existing oauth_failed callback flow, and preserve the current handling for
non-200 responses. Add regression tests covering the timeout and invalid-JSON
paths.

In `@ddpui/ddpairbyte/airbytehelpers.py`:
- Line 93: Update the flow around google_oauth_service.redeem_ref to atomically
claim the OAuth ref without permanently deleting it, then finalize its
consumption only after environment lookup and the Airbyte persistence succeeds.
On any configuration or Airbyte failure, release or restore the claim so the
refresh token remains redeemable.
- Around line 100-103: Before the payload.sourceId branch invokes update_source,
validate that the source belongs to the caller’s workspace and matches the
requested source definition, using the workspace context and existing
ownership-validation flow. Reject cross-workspace or mismatched sources before
redeeming the ref or modifying the source, and add a test covering rejection of
another workspace’s source ID.
🪄 Autofix (Beta)

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

Run ID: b7fb51dd-0ca3-40aa-909d-17ddc955458a

📥 Commits

Reviewing files that changed from the base of the PR and between f03abed and bff813a.

📒 Files selected for processing (8)
  • .env.template
  • ddpui/api/airbyte_api.py
  • ddpui/core/oauth/__init__.py
  • ddpui/core/oauth/google_oauth_provider.py
  • ddpui/core/oauth/google_oauth_service.py
  • ddpui/ddpairbyte/airbytehelpers.py
  • ddpui/ddpairbyte/schema.py
  • ddpui/tests/api_tests/test_airbyte_api.py

Comment thread ddpui/core/oauth/google_oauth_service.py Outdated
it into `payload.config`, and saves the source. The refresh_token never travels through
the browser. With `payload.sourceId` set it updates that source (re-authenticate);
otherwise it creates a new one."""
refresh_token = google_oauth_service.redeem_ref(orguser, payload.ref, payload.sourceDefId)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not consume the OAuth ref before persistence succeeds.

redeem_ref() performs GETDEL before environment lookup and the Airbyte write. Any configuration or Airbyte failure permanently loses the refresh token and forces the user through consent again. Use an atomic claim/finalize flow, releasing or restoring the claim on failure.

🤖 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 `@ddpui/ddpairbyte/airbytehelpers.py` at line 93, Update the flow around
google_oauth_service.redeem_ref to atomically claim the OAuth ref without
permanently deleting it, then finalize its consumption only after environment
lookup and the Airbyte persistence succeeds. On any configuration or Airbyte
failure, release or restore the claim so the refresh token remains redeemable.

Comment on lines +100 to +103
if payload.sourceId:
source = airbyte_service.update_source(
payload.sourceId, payload.name, config, payload.sourceDefId
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Verify that sourceId belongs to the caller's workspace before updating.

The client controls sourceId, while update_source() accepts no workspace context. A user who obtains another workspace's source ID can overwrite that source's name, configuration, and credentials. Validate workspace ownership and source definition before redeeming the ref or invoking the update; add a cross-workspace rejection test.

🤖 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 `@ddpui/ddpairbyte/airbytehelpers.py` around lines 100 - 103, Before the
payload.sourceId branch invokes update_source, validate that the source belongs
to the caller’s workspace and matches the requested source definition, using the
workspace context and existing ownership-validation flow. Reject cross-workspace
or mismatched sources before redeeming the ref or modifying the source, and add
a test covering rejection of another workspace’s source ID.

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

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 `@ddpui/core/oauth/google_oauth_service.py`:
- Around line 175-183: Validate that the value assigned to tokens in the Google
OAuth exchange is a mapping/object before checking for "refresh_token" or
calling list(tokens). For valid JSON values such as null or a list, log the
malformed response and raise the same intended OAuth failure HttpError instead
of allowing TypeError to escape; preserve the existing missing-refresh-token
handling for valid objects.
🪄 Autofix (Beta)

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

Run ID: 3ad62120-6392-4198-8c82-e74cfa41c61e

📥 Commits

Reviewing files that changed from the base of the PR and between bff813a and 964bfdb.

📒 Files selected for processing (2)
  • ddpui/core/oauth/google_oauth_service.py
  • ddpui/tests/api_tests/test_airbyte_api.py

Comment on lines +175 to +183
try:
tokens = response.json()
except ValueError as err:
# a 200 with a non-JSON body — treat as a failed exchange, not a 500 crash
logger.error("google token exchange returned non-JSON body: %s", response.text[:200])
raise HttpError(500, "failed to complete the oauth flow") from err
if "refresh_token" not in tokens:
logger.error("google token exchange returned no refresh_token: keys=%s", list(tokens))
raise HttpError(400, "oauth did not return a refresh token")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate that the token response is a JSON object.

A 200 response containing valid JSON such as null or a list causes an uncaught TypeError at Line 181 instead of the intended oauth_failed redirect.

Proposed fix
     try:
         tokens = response.json()
     except ValueError as err:
         logger.error("google token exchange returned non-JSON body: %s", response.text[:200])
         raise HttpError(500, "failed to complete the oauth flow") from err
+    if not isinstance(tokens, dict):
+        logger.error(
+            "google token exchange returned unexpected JSON type: %s",
+            type(tokens).__name__,
+        )
+        raise HttpError(500, "failed to complete the oauth flow")
     if "refresh_token" not in tokens:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
tokens = response.json()
except ValueError as err:
# a 200 with a non-JSON body — treat as a failed exchange, not a 500 crash
logger.error("google token exchange returned non-JSON body: %s", response.text[:200])
raise HttpError(500, "failed to complete the oauth flow") from err
if "refresh_token" not in tokens:
logger.error("google token exchange returned no refresh_token: keys=%s", list(tokens))
raise HttpError(400, "oauth did not return a refresh token")
try:
tokens = response.json()
except ValueError as err:
logger.error("google token exchange returned non-JSON body: %s", response.text[:200])
raise HttpError(500, "failed to complete the oauth flow") from err
if not isinstance(tokens, dict):
logger.error(
"google token exchange returned unexpected JSON type: %s",
type(tokens).__name__,
)
raise HttpError(500, "failed to complete the oauth flow")
if "refresh_token" not in tokens:
🤖 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 `@ddpui/core/oauth/google_oauth_service.py` around lines 175 - 183, Validate
that the value assigned to tokens in the Google OAuth exchange is a
mapping/object before checking for "refresh_token" or calling list(tokens). For
valid JSON values such as null or a list, log the malformed response and raise
the same intended OAuth failure HttpError instead of allowing TypeError to
escape; preserve the existing missing-refresh-token handling for valid objects.

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.

1 participant