Skip to content

fix: surface permission sync issues requiring user action#1484

Open
brendan-kellam wants to merge 3 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 3 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

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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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
previousHasPendingFirstSync === true && status.hasPendingFirstSync === false;
const issueResolved = previousIssueCount !== undefined &&
previousIssueCount > 0 && status.issues.length === 0;
if (initialSyncCompleted || issueResolved) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale cards after partial issue fix

Medium Severity

router.refresh() runs only when the issue count drops to zero. If one of several accounts is fixed, the banner can update from polling while linked-account cards keep SSR props that still show permissionSyncIssue for the already-recovered account.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c149678. Configure here.

providerType: account.providerType,
reason: account.permissionSyncIssue,
occurredAt: account.permissionSyncIssueAt?.toISOString() ?? 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.

Issues ignore sync enabled flag

Medium Severity

getPermissionSyncStatus returns permissionSyncIssue rows even when PERMISSION_SYNC_ENABLED is false, while reauth scheduling and the worker trigger both require that flag. Leftover issues can keep showing action-required UI that reconnect cannot clear.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c149678. Configure here.

router.refresh();
}
}, [hasPendingFirstSync, previousHasPendingFirstSync, router]);
}, [status.hasPendingFirstSync, status.issues.length, previousHasPendingFirstSync, previousIssueCount, 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.

Banner hides after poll failure

Medium Severity

Returning null when isError is true drops the action-required banner even though status still holds initialData or the last successful poll with issues. A failed background refetch can therefore hide the warning that private repository access was cleared, leaving users without the recovery path this PR adds.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c149678. Configure here.

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.

@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 1 potential issue.

There are 5 total unresolved issues (including 4 from previous reviews).

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 2c03c15. Configure here.

<RefreshCw className="h-4 w-4 mr-2" />
Refresh Permissions
</DropdownMenuItem>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Recovery blocks manual permission retry

High Severity

The UI hides the "Refresh Permissions" option when linkedAccount.permissionSyncIssue is set. This prevents users from manually retrying a permission sync after a successful reauthorization if the subsequent automatic recovery sync fails or is delayed, forcing unnecessary full reconnects.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2c03c15. Configure here.

@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

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