Detect GitHub OAuth access revocation via repeated sync failures, and let users re-authorize

What does this MR do and why

When a user revokes GitLab's OAuth access on GitHub's side, there's no webhook telling us — GitHub OAuth apps don't get a revocation event the way GitHub Apps do (installation.deleted). Sync just silently stops working with no signal to anyone. This MR adds detection for that by watching for repeated auth failures — after 5 in a row, we move the repository into a "disconnected", read-only state and record what it needs to restore to once the user re-authorizes.

What's in this MR:

  1. A consecutive_auth_failures counter column on import_sync_repositories.
  2. New Import::Sync::Repository model methods: record_auth_failure! increments the counter and disconnects the repository once it hits 5 in a row, clear_auth_failures! resets it after a successful call, disconnect! moves the repository into the disconnected/read-only state, and restore! reverses that.
  3. WebhookManagementService#enable now calls record_auth_failure! when creating the GitHub webhook fails with a 401. This is the only place in the codebase today that can actually hit this error, since the continuous-sync/reconciliation engine itself (a separate work item, still unbuilt) doesn't exist yet.
  4. A new Import::Sync::Github::Oauth::ReauthorizeService: takes a freshly-obtained OAuth client after the user re-authorizes, checks it's for the same GitHub account (so a different GitHub user can't hijack the reconnect), restores the repository's state, and re-creates the webhook.

What this MR deliberately leaves out: there's no new controller, route, or UI button for a user to actually trigger re-authorization. The settings panel meant to host that button is a separate work item that hasn't been built yet, so there's nowhere to wire it in. ReauthorizeService is ready to be called once that panel exists.

The failure threshold (5 in a row) is a judgment call — the original issue left the exact number open on purpose, to tolerate one-off blips without over-triggering.

All of this sits behind the existing github_continuous_import feature flag (wip type, default off), so there's no user-facing changelog entry beyond the one on the database migration (migrations always get one).

Note on testing: I wrote specs for all new model methods, the webhook service's new 401 handling, and the new re-authorize service, but could not run them locally — my local dev environment's gem bundle is out of sync (pre-existing issue, unrelated to this change) and the caproni exec wrapper is broken. This needs a real CI run to confirm the specs pass.

References

Part of gitlab-org#23553 (GitHub Sync - Experiment) Implements #630404 (closed) Depends on (already merged) #630400 (closed) Follow-up needed once #628593 lands: wire ReauthorizeService up to a controller/route and the settings panel's "re-authorize" action.

Screenshots or screen recordings

Not applicable — backend-only change, no UI.

How to set up and validate locally

  1. In a Rails console, enable the feature flag: Feature.enable(:github_continuous_import)
  2. Create a test repository: repo = FactoryBot.create(:import_sync_repository)
  3. Call repo.record_auth_failure! five times in a row, then reload: repo.reload.authority_state should be "disconnected", and repo.disconnect_reason should be "token_revoked"
  4. Call repo.restore! and confirm repo.reload.authority_state is back to "provider_canonical", with disconnect_reason and restore_authority_state both nil
  5. Run the new/changed specs:
    bundle exec rspec ee/spec/models/import/sync/repository_spec.rb
    bundle exec rspec ee/spec/services/import/sync/github/oauth/webhook_management_service_spec.rb
    bundle exec rspec ee/spec/services/import/sync/github/oauth/reauthorize_service_spec.rb

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by George Koltsov

Merge request reports

Loading
Loading