Skip to content

fix: surface permission sync issues requiring user action#1484

Open
brendan-kellam wants to merge 5 commits into
brendan/classify-permission-sync-errorsfrom
brendan/permission-sync-action-required-ux
Open

fix: surface permission sync issues requiring user action#1484
brendan-kellam wants to merge 5 commits into
brendan/classify-permission-sync-errorsfrom
brendan/permission-sync-action-required-ux

Conversation

@brendan-kellam

@brendan-kellam brendan-kellam commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Stack created with GitHub Stacks CLI

Fixes SOU-1560
Fixes SOU-1177

image image image

Summary

  • persist a structured account issue when permission sync fails closed
  • clear the issue only after permissions sync successfully
  • show a persistent action-required banner and unhealthy linked-account state
  • schedule permission recovery immediately after OAuth reauthentication
  • stop treating transient token refresh errors as reconnect-required UX

Testing

  • yarn workspace @sourcebot/backend test --run (201 tests)
  • yarn workspace @sourcebot/web test --run (1,095 tests)
  • backend and database package builds
  • Prisma schema validation
  • ESLint on touched web files

Part of stack #1483. Depends on #1482.


Note

Medium Risk
Changes EE repository permission state and auth recovery flows; behavior is scoped to classified fail-closed sync failures and is covered by new lifecycle tests, but incorrect issue classification could still mislead users or delay access restoration.

Overview
When account permission sync fails closed and cached repo access is cleared, the backend now persists a structured issue on the linked account (REAUTHENTICATION_REQUIRED or INSUFFICIENT_SCOPE) in the same transaction as the permission wipe, and clears that issue only after a successful sync.

The permission sync status API and app banner expose these issues (non-dismissible warning with a link to linked accounts), including a syncing state while recovery jobs run. Linked account cards use the new issue type instead of generic token-refresh error text and steer users toward reconnect/reauthorize rather than “Refresh Permissions.”

After OAuth reauthentication, if the account still had an open permission sync issue, the web app queues a permission sync via a shared server-side worker client (also used by the manual trigger action).

Reviewed by Cursor Bugbot for commit 2c03c15. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added clear warnings when permission synchronization requires reauthentication or additional access.
    • Added guided recovery through linked-account settings, including reconnect and permission-review actions.
    • Permission sync status now updates automatically and clears after successful recovery.
    • Successful reauthentication can automatically retry permission synchronization.
  • Bug Fixes

    • Improved handling of permanent versus temporary synchronization failures.
    • Prevented stale repository access from remaining available after permanent permission failures.
  • Documentation

    • Updated the unreleased changelog with the new warnings and recovery guidance.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: aaf8f9ce-a01f-4690-8435-b543fbb6d19a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Permission synchronization now persists classified account issues, clears cached permissions on permanent failures, exposes issue status through APIs, retries after reauthentication, and presents actionable banner and linked-account recovery states.

Changes

Permission sync issue lifecycle

Layer / File(s) Summary
Persist and classify sync issues
packages/db/prisma/schema.prisma, packages/db/prisma/migrations/..., packages/backend/src/ee/accountPermissionSyncer.ts, packages/backend/src/ee/accountPermissionSyncer.test.ts
Adds issue enum and account fields; classified failures transactionally clear cached permissions and record issue metadata, while successful syncs clear it.
Expose status and trigger recovery
packages/web/src/app/api/(server)/ee/permissionSyncStatus/*, packages/web/src/features/workerApi/*, packages/web/src/auth.ts
Returns structured account issues, centralizes worker requests, and schedules permission sync after reauthentication.
Render permission-sync banners
packages/web/src/app/(app)/components/banners/*, packages/web/src/app/(app)/layout.tsx
Passes issue status into the banner, which displays syncing or recovery guidance and refreshes when issues resolve.
Show linked-account recovery actions
packages/web/src/ee/features/sso/actions.ts, packages/web/src/ee/features/sso/components/linkedAccountProviderCard.*
Replaces token-refresh error state with permission-sync issue state and renders reconnect or scope guidance.
Document the behavior
CHANGELOG.md
Adds an Unreleased changelog entry for action-required warnings and guided recovery.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WebAuth
  participant WorkerApi
  participant PermissionSyncer
  participant Database
  participant PermissionSyncBanner
  User->>WebAuth: reauthenticate linked account
  WebAuth->>WorkerApi: request account permission sync
  WorkerApi->>PermissionSyncer: start sync job
  PermissionSyncer->>Database: record or clear permission-sync issue
  PermissionSyncBanner->>Database: poll permission-sync status
  Database-->>PermissionSyncBanner: issue or syncing status
  PermissionSyncBanner-->>User: show recovery or progress banner
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: surfacing permission sync issues that require user action.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/permission-sync-action-required-ux

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.

@github-actions

This comment has been minimized.

@brendan-kellam brendan-kellam changed the title brendan/permission sync action required ux fix: surface permission sync issues requiring user action Jul 22, 2026
Comment thread packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts
update: {
permissionSyncedAt: new Date(),
permissionSyncIssue: null,
permissionSyncIssueAt: null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Completed job clears newer issue

High Severity

onJobCompleted always nulls permissionSyncIssue for the account, with no check against a newer fail-closed update. Concurrent jobs for the same account are allowed (schedulePermissionSyncForAccount never dedupes; default concurrency is 8), so a later successful completion can erase an issue after another job already cleared permissions, leaving empty access and no banner.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c149678. Configure here.

@brendan-kellam

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

🧹 Nitpick comments (2)
packages/web/src/features/workerApi/actions.ts (1)

64-74: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Log the underlying error before returning a generic failure.

The catch block discards the actual error (network failure, timeout, schema mismatch), unlike the equivalent scheduling path in auth.ts which logs it. This makes production failures hard to diagnose.

♻️ Suggested fix
 export const triggerAccountPermissionSync = async (accountId: string) => sew(() =>
     withAuth(({ role }) =>
         withMinimumOrgRole(role, OrgRole.MEMBER, async () => {
             try {
                 return await requestAccountPermissionSync(accountId);
-            } catch {
+            } catch (error) {
+                logger.error(`Failed to trigger account permission sync for account ${accountId}: ${error instanceof Error ? error.message : String(error)}`);
                 return unexpectedError('Failed to trigger account permission sync');
             }
         })
     )
 );
🤖 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 `@packages/web/src/features/workerApi/actions.ts` around lines 64 - 74, Update
the catch block in triggerAccountPermissionSync to capture the underlying error
and log it before returning the existing generic unexpectedError response,
matching the diagnostic behavior used by the equivalent scheduling path in
auth.ts.
packages/web/src/auth.ts (1)

228-240: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider not awaiting the recovery sync inside events.signIn.

requestAccountPermissionSync is awaited directly in the sign-in event, which Auth.js awaits before completing the response — this can add up to the client's 5s timeout to sign-in latency for accounts with an existing permission issue if the worker is slow. Since this is a fire-and-forget scheduling call (errors are already caught/logged and don't affect sign-in outcome), consider not awaiting it so sign-in isn't delayed by worker availability.

♻️ Suggested fix
                 if (
                     updatedAccount.permissionSyncIssue !== null &&
                     env.PERMISSION_SYNC_ENABLED === 'true'
                 ) {
-                    try {
-                        if (await hasEntitlement('permission-syncing')) {
-                            await requestAccountPermissionSync(updatedAccount.id);
-                        }
-                    } catch (error) {
-                        const message = error instanceof Error ? error.message : String(error);
-                        logger.error(`Failed to schedule permission sync after reauthentication for account ${updatedAccount.id}: ${message}`);
-                    }
+                    hasEntitlement('permission-syncing')
+                        .then((entitled) => entitled ? requestAccountPermissionSync(updatedAccount.id) : undefined)
+                        .catch((error) => {
+                            const message = error instanceof Error ? error.message : String(error);
+                            logger.error(`Failed to schedule permission sync after reauthentication for account ${updatedAccount.id}: ${message}`);
+                        });
                 }
🤖 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 `@packages/web/src/auth.ts` around lines 228 - 240, Update the recovery sync
block in the events.signIn flow to invoke requestAccountPermissionSync without
awaiting its completion, while retaining the existing error capture and
logger.error handling so failures remain logged without delaying sign-in.
🤖 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 `@packages/backend/src/ee/accountPermissionSyncer.ts`:
- Around line 259-273: Update the fail-closed cleanup warning in the account
permission sync flow to remove account.user.email from the log message,
retaining the account.id and existing cleanup details and error message. Do not
alter the transaction or cleanup behavior.

---

Nitpick comments:
In `@packages/web/src/auth.ts`:
- Around line 228-240: Update the recovery sync block in the events.signIn flow
to invoke requestAccountPermissionSync without awaiting its completion, while
retaining the existing error capture and logger.error handling so failures
remain logged without delaying sign-in.

In `@packages/web/src/features/workerApi/actions.ts`:
- Around line 64-74: Update the catch block in triggerAccountPermissionSync to
capture the underlying error and log it before returning the existing generic
unexpectedError response, matching the diagnostic behavior used by the
equivalent scheduling path in auth.ts.
🪄 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: 99b97fda-b5b7-47a9-a62e-c231dc3cc2ad

📥 Commits

Reviewing files that changed from the base of the PR and between a985fac and 2c03c15.

📒 Files selected for processing (19)
  • CHANGELOG.md
  • packages/backend/src/ee/accountPermissionSyncer.test.ts
  • packages/backend/src/ee/accountPermissionSyncer.ts
  • packages/db/prisma/migrations/20260722144824_add_account_permission_sync_issue/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/web/src/app/(app)/components/banners/bannerResolver.test.ts
  • packages/web/src/app/(app)/components/banners/bannerResolver.tsx
  • packages/web/src/app/(app)/components/banners/permissionSyncBanner.test.tsx
  • packages/web/src/app/(app)/components/banners/permissionSyncBanner.tsx
  • packages/web/src/app/(app)/layout.tsx
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.test.ts
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts
  • packages/web/src/auth.ts
  • packages/web/src/ee/features/sso/actions.ts
  • packages/web/src/ee/features/sso/components/linkedAccountProviderCard.test.tsx
  • packages/web/src/ee/features/sso/components/linkedAccountProviderCard.tsx
  • packages/web/src/features/workerApi/actions.ts
  • packages/web/src/features/workerApi/client.server.test.ts
  • packages/web/src/features/workerApi/client.server.ts

Comment on lines +259 to +273
const details = PERMISSION_CLEANUP_DETAILS[cleanupDecision.reason];
const [{ count }] = await this.db.$transaction([
this.db.accountToRepoPermission.deleteMany({
where: { accountId: account.id },
}),
this.db.account.update({
where: { id: account.id },
data: {
permissionSyncIssue: details.issue,
permissionSyncIssueAt: new Date(),
},
}),
]);
const message = error instanceof Error ? error.message : String(error);
const reason = PERMISSION_CLEANUP_REASON_MESSAGES[cleanupDecision.reason];
logger.warn(`Cleared ${count} permission row(s) for account ${account.id} (user ${account.user.email ?? 'unknown'}) — fail-closed cleanup triggered by ${reason}: ${message}`);
logger.warn(`Cleared ${count} permission row(s) for account ${account.id} (user ${account.user.email ?? 'unknown'}) — fail-closed cleanup triggered by ${details.message}: ${message}`);

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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Avoid logging user email in the fail-closed cleanup log line.

account.user.email is included in the warn log for fail-closed cleanup. This mirrors existing (unchanged) patterns elsewhere in the file, but static analysis flags it as PII exposure in logs (CWE-532). Consider logging account.id only, or a redacted/hashed identifier.

🔒 Suggested fix
-                logger.warn(`Cleared ${count} permission row(s) for account ${account.id} (user ${account.user.email ?? 'unknown'}) — fail-closed cleanup triggered by ${details.message}: ${message}`);
+                logger.warn(`Cleared ${count} permission row(s) for account ${account.id} — fail-closed cleanup triggered by ${details.message}: ${message}`);
📝 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
const details = PERMISSION_CLEANUP_DETAILS[cleanupDecision.reason];
const [{ count }] = await this.db.$transaction([
this.db.accountToRepoPermission.deleteMany({
where: { accountId: account.id },
}),
this.db.account.update({
where: { id: account.id },
data: {
permissionSyncIssue: details.issue,
permissionSyncIssueAt: new Date(),
},
}),
]);
const message = error instanceof Error ? error.message : String(error);
const reason = PERMISSION_CLEANUP_REASON_MESSAGES[cleanupDecision.reason];
logger.warn(`Cleared ${count} permission row(s) for account ${account.id} (user ${account.user.email ?? 'unknown'}) — fail-closed cleanup triggered by ${reason}: ${message}`);
logger.warn(`Cleared ${count} permission row(s) for account ${account.id} (user ${account.user.email ?? 'unknown'}) — fail-closed cleanup triggered by ${details.message}: ${message}`);
const details = PERMISSION_CLEANUP_DETAILS[cleanupDecision.reason];
const [{ count }] = await this.db.$transaction([
this.db.accountToRepoPermission.deleteMany({
where: { accountId: account.id },
}),
this.db.account.update({
where: { id: account.id },
data: {
permissionSyncIssue: details.issue,
permissionSyncIssueAt: new Date(),
},
}),
]);
const message = error instanceof Error ? error.message : String(error);
logger.warn(`Cleared ${count} permission row(s) for account ${account.id} — fail-closed cleanup triggered by ${details.message}: ${message}`);
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 272-272: Avoid logging sensitive data
Context: logger.warn(Cleared ${count} permission row(s) for account ${account.id} (user ${account.user.email ?? 'unknown'}) — fail-closed cleanup triggered by ${details.message}: ${message})
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)

🤖 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 `@packages/backend/src/ee/accountPermissionSyncer.ts` around lines 259 - 273,
Update the fail-closed cleanup warning in the account permission sync flow to
remove account.user.email from the log message, retaining the account.id and
existing cleanup details and error message. Do not alter the transaction or
cleanup behavior.

Source: Linters/SAST tools

@brendan-kellam
brendan-kellam force-pushed the brendan/classify-permission-sync-errors branch from a985fac to 3cc17b8 Compare July 23, 2026 18:02
@brendan-kellam
brendan-kellam force-pushed the brendan/permission-sync-action-required-ux branch from 2c03c15 to 5873631 Compare July 23, 2026 18:02

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5873631. Configure here.

router.refresh();
}
}, [syncJobId, syncStatusData, displayName, toast, router]);
}, [syncJobId, syncJobStatus, displayName, toast, router]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sync status poll can hang UI

Medium Severity

When getAccountSyncStatus fails, the refetchInterval stops polling due to missing status data. However, isSyncing remains true, causing the UI to get stuck on 'Syncing...' without a terminal toast or resolution until a page reload.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5873631. Configure here.

description="Sourcebot is syncing what repositories you have access to from a code host. This may take a minute."
/>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

First sync hides action-required banner

Low Severity

When any account still has hasPendingFirstSync, the banner always renders the syncing state and skips the action-required UI. A second linked account with a non-syncing permissionSyncIssue then stays hidden behind “Syncing repository access,” so reconnect guidance is deferred until unrelated first-sync work finishes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5873631. Configure here.

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