Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions db/migrations/20261023090000_github_app_shared_installations.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- migrate:up
-- One GitHub App installation can be connected to several LiveReview orgs (one connector per org), e.g. when two
-- people from the same GitHub organization each sign up. Each org still has at most one connector per installation.
DROP INDEX IF EXISTS uq_integration_tokens_github_app_installation;
CREATE UNIQUE INDEX IF NOT EXISTS uq_integration_tokens_github_app_installation_org
ON integration_tokens (provider_url, provider_app_id, org_id)
WHERE token_type = 'GITHUB_APP';

-- migrate:down
-- Fails while an installation is connected to more than one org; remove the extra connectors first.
DROP INDEX IF EXISTS uq_integration_tokens_github_app_installation_org;
CREATE UNIQUE INDEX IF NOT EXISTS uq_integration_tokens_github_app_installation
ON integration_tokens (provider_url, provider_app_id)
WHERE token_type = 'GITHUB_APP';
7 changes: 4 additions & 3 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -6083,10 +6083,10 @@ CREATE UNIQUE INDEX river_job_unique_idx ON public.river_job USING btree (unique


--
-- Name: uq_integration_tokens_github_app_installation; Type: INDEX; Schema: public; Owner: -
-- Name: uq_integration_tokens_github_app_installation_org; Type: INDEX; Schema: public; Owner: -
--

CREATE UNIQUE INDEX uq_integration_tokens_github_app_installation ON public.integration_tokens USING btree (provider_url, provider_app_id) WHERE (token_type = 'GITHUB_APP'::text);
CREATE UNIQUE INDEX uq_integration_tokens_github_app_installation_org ON public.integration_tokens USING btree (provider_url, provider_app_id, org_id) WHERE (token_type = 'GITHUB_APP'::text);


--
Expand Down Expand Up @@ -7309,4 +7309,5 @@ INSERT INTO public.schema_migrations (version) VALUES
('20261017100000'),
('20261020090000'),
('20261022120000'),
('20261022130000');
('20261022130000'),
('20261023090000');
41 changes: 41 additions & 0 deletions internal/api/auto_webhook_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"log"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -116,6 +117,11 @@ func (awi *AutoWebhookInstaller) processAutoInstallation(ctx context.Context, co
// waiting for the first periodic reconciliation sweep.
func (awi *AutoWebhookInstaller) syncRepositoriesAndQueueBackfill(ctx context.Context, connectorID int, connector *ConnectorDetails) error {
store := pullrequests.NewStore(awi.db)
// A user-scoped GitHub App connector only gets the repositories its user can access.
allowed, scoped, err := userScopedRepoIDs(connector)
if err != nil {
return err
}

cursor := ""
total := 0
Expand All @@ -138,6 +144,9 @@ func (awi *AutoWebhookInstaller) syncRepositoriesAndQueueBackfill(ctx context.Co
}

for _, repo := range page.Repositories {
if scoped && !allowed[repo.ProviderRepoID] {
continue
}
repositoryID, err := store.UpsertRepository(pullrequests.RepositoryUpsert{
OrgID: connector.OrgID,
ConnectorID: int64(connectorID),
Expand Down Expand Up @@ -169,10 +178,42 @@ func (awi *AutoWebhookInstaller) syncRepositoriesAndQueueBackfill(ctx context.Co
cursor = page.NextCursor
}

if scoped {
keep := make([]string, 0, len(allowed))
for id := range allowed {
keep = append(keep, id)
}
if err := store.DeleteRepositoriesByConnectorExcept(int64(connectorID), keep); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: critical

DeleteRepositoriesByConnectorExcept with an empty keep list deletes every repository for the connector. This is destructive and runs whenever allowed parses to empty.

Suggestions:

  1. Skip the prune call when keep is empty, or guard it behind a positive allowed count.
  2. Log the number of pruned repositories for auditability.

return fmt.Errorf("failed to drop repositories the user can no longer access: %w", err)
}
}

log.Printf("Queued initial PR/MR backfill for %d repositories (connector %d)", total, connectorID)
return nil
}

// userScopedRepoIDs returns the provider repository ids a user-scoped GitHub App connector may sync; scoped is
// false for every other connector (installer App connectors and PAT/OAuth ones sync everything they list). An
// unreadable list is an error rather than "no repositories", because the sync drops repositories not on it.
func userScopedRepoIDs(connector *ConnectorDetails) (allowed map[string]bool, scoped bool, err error) {
if connector.TokenType != storagegithub.TokenTypeGitHubApp || connector.Metadata["repo_scope"] != storagegithub.RepoScopeUser {
return nil, false, nil
}
ids, ok := connector.Metadata["allowed_repo_ids"].([]interface{})
if !ok {
return nil, true, fmt.Errorf("connector %d is user-scoped but has no readable allowed_repo_ids", connector.ID)
}
allowed = map[string]bool{}
for _, raw := range ids {
id, ok := raw.(float64)
if !ok {
return nil, true, fmt.Errorf("connector %d has a non-numeric allowed_repo_ids entry %v", connector.ID, raw)
}
allowed[strconv.FormatInt(int64(id), 10)] = true
}
return allowed, true, nil
}

// listAppInstallationRepositories lists one page of repositories for a GitHub App connector, using a valid
// (possibly just-minted) installation token rather than whatever pat_token held when the connector was loaded.
func (awi *AutoWebhookInstaller) listAppInstallationRepositories(ctx context.Context, connector *ConnectorDetails, cursor string) (*providers.RepositoryPage, error) {
Expand Down
Loading
Loading