CVE-2024-31455: Insufficient Information (NVD-CWE-noinfo)
value: Minder by Stacklok is an open source software supply chain security platform. A refactoring in commit `5c381cf` added the ability to get GitHub repositories registered to a project without specifying a specific provider.  Unfortunately, the SQL query for doing so was missing parenthesis, and would select a random repository. This issue is patched in pull request 2941. As a workaround, revert prior to `5c381cf`, or roll forward past `2eb94e7`.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-09T17:16Z
Repository: https://github.com/stacklok/minder
Language: Go
Files changed: cmd/cli/app/artifact/artifact.go, cmd/cli/app/repo/repo.go, cmd/server/app/webhook_update.go, database/query/repositories.sql, internal/controlplane/common.go, internal/controlplane/handlers_artifacts.go, internal/controlplane/handlers_oauth.go, internal/controlplane/handlers_profile.go, internal/controlplane/handlers_repositories.go, internal/controlplane/handlers_repositories_test.go, internal/db/repositories.sql.go, internal/db/repositories_test.go, internal/providers/github/common.go, internal/reconcilers/run_profile.go, internal/repositories/github/mock/service.go, internal/repositories/github/service.go, internal/repositories/github/service_test.go
Commit date: 2024-04-01 15:28:12 -0700
Commit message: Make provider an optional filter (#2871)

Ref #2522
Diff: diff --git a/cmd/cli/app/artifact/artifact.go b/cmd/cli/app/artifact/artifact.go
index 521c0a414..44034af40 100644
--- a/cmd/cli/app/artifact/artifact.go
+++ b/cmd/cli/app/artifact/artifact.go
@@ -18,11 +18,10 @@ package artifact
 
 import (
 	"github.com/spf13/cobra"
 
 	"github.com/stacklok/minder/cmd/cli/app"
-	ghclient "github.com/stacklok/minder/internal/providers/github/oauth"
 )
 
 // ArtifactCmd is the artifact subcommand
 var ArtifactCmd = &cobra.Command{
 	Use:   "artifact",
@@ -34,8 +33,8 @@ var ArtifactCmd = &cobra.Command{
 }
 
 func init() {
 	app.RootCmd.AddCommand(ArtifactCmd)
 	// Flags for all subcommands
-	ArtifactCmd.PersistentFlags().StringP("provider", "p", ghclient.Github, "Name of the provider, i.e. github")
+	ArtifactCmd.PersistentFlags().StringP("provider", "p", "", "Name of the provider, i.e. github")
 	ArtifactCmd.PersistentFlags().StringP("project", "j", "", "ID of the project")
 }
diff --git a/cmd/cli/app/repo/repo.go b/cmd/cli/app/repo/repo.go
index 74b0d4c33..1a87398c9 100644
--- a/cmd/cli/app/repo/repo.go
+++ b/cmd/cli/app/repo/repo.go
@@ -18,11 +18,10 @@ package repo
 
 import (
 	"github.com/spf13/cobra"
 
 	"github.com/stacklok/minder/cmd/cli/app"
-	ghclient "github.com/stacklok/minder/internal/providers/github/oauth"
 )
 
 // RepoCmd is the root command for the repo subcommands
 var RepoCmd = &cobra.Command{
 	Use:   "repo",
@@ -34,8 +33,8 @@ var RepoCmd = &cobra.Command{
 }
 
 func init() {
 	app.RootCmd.AddCommand(RepoCmd)
 	// Flags for all subcommands
-	RepoCmd.PersistentFlags().StringP("provider", "p", ghclient.Github, "Name of the provider, i.e. github")
+	RepoCmd.PersistentFlags().StringP("provider", "p", "", "Name of the provider, i.e. github")
 	RepoCmd.PersistentFlags().StringP("project", "j", "", "ID of the project")
 }
diff --git a/cmd/server/app/webhook_update.go b/cmd/server/app/webhook_update.go
index 5c249fa83..66ad38ccf 100644
--- a/cmd/server/app/webhook_update.go
+++ b/cmd/server/app/webhook_update.go
@@ -139,11 +139,14 @@ func updateGithubWebhooks(
 	webhookHost string,
 	secret string,
 ) error {
 	repos, err := store.ListRegisteredRepositoriesByProjectIDAndProvider(ctx,
 		db.ListRegisteredRepositoriesByProjectIDAndProviderParams{
-			Provider:  provider.Name,
+			Provider: sql.NullString{
+				String: provider.Name,
+				Valid:  true,
+			},
 			ProjectID: provider.ProjectID,
 		})
 	if err != nil {
 		return fmt.Errorf("unable to list registered repositories: %w", err)
 	}
diff --git a/database/query/repositories.sql b/database/query/repositories.sql
index e47655ff7..91f28dc1a 100644
--- a/database/query/repositories.sql
+++ b/database/query/repositories.sql
@@ -18,29 +18,33 @@ INSERT INTO repositories (
 
 -- name: GetRepositoryByRepoID :one
 SELECT * FROM repositories WHERE repo_id = $1;
 
 -- name: GetRepositoryByRepoName :one
-SELECT * FROM repositories WHERE provider = $1 AND repo_owner = $2 AND repo_name = $3 AND project_id = $4;
+SELECT * FROM repositories
+    WHERE repo_owner = $1 AND repo_name = $2 AND project_id = $3
+    AND lower(provider) = lower(sqlc.narg('provider')::text) OR sqlc.narg('provider')::text IS NULL;
 
 -- avoid using this, where possible use GetRepositoryByIDAndProject instead
 -- name: GetRepositoryByID :one
 SELECT * FROM repositories WHERE id = $1;
 
 -- name: GetRepositoryByIDAndProject :one
 SELECT * FROM repositories WHERE id = $1 AND project_id = $2;
 
 -- name: ListRepositoriesByProjectID :many
 SELECT * FROM repositories
-WHERE provider = $1 AND project_id = $2
+WHERE project_id = $1
   AND (repo_id >= sqlc.narg('repo_id') OR sqlc.narg('repo_id') IS NULL)
+  AND lower(provider) = lower(COALESCE(sqlc.narg('provider'), provider)::text)
 ORDER BY project_id, provider, repo_id
 LIMIT sqlc.narg('limit')::bigint;
 
 -- name: ListRegisteredRepositoriesByProjectIDAndProvider :many
 SELECT * FROM repositories
-WHERE provider = $1 AND project_id = $2 AND webhook_id IS NOT NULL
+WHERE project_id = $1 AND webhook_id IS NOT NULL
+    AND lower(provider) = lower(sqlc.narg('provider')::text) OR sqlc.narg('provider')::text IS NULL
 ORDER BY repo_name;
 
 -- name: DeleteRepository :exec
 DELETE FROM repositories
 WHERE id = $1;
diff --git a/internal/controlplane/common.go b/internal/controlplane/common.go
index cf4a5b354..e5e2a450c 100644
--- a/internal/controlplane/common.go
+++ b/internal/controlplane/common.go
@@ -25,14 +25,17 @@ import (
 	"github.com/google/uuid"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 
 	"github.com/stacklok/minder/internal/db"
+	"github.com/stacklok/minder/internal/providers/github/oauth"
 	"github.com/stacklok/minder/internal/util"
 	pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
 )
 
+const defaultProvider = oauth.Github
+
 // HasProtoContext is an interface that can be implemented by a request
 type HasProtoContext interface {
 	GetContext() *pb.Context
 }
 
@@ -58,14 +61,14 @@ func filteredResultNotFoundError(name sql.NullString, trait db.NullProviderType)
 }
 
 func getProviderFromRequestOrDefault(
 	ctx context.Context,
 	store db.Store,
-	in HasProtoContext,
+	providerName string,
 	projectId uuid.UUID,
 ) (db.Provider, error) {
-	name := getNameFilterParam(in.GetContext())
+	name := getNameFilterParam(providerName)
 	providers, err := findProvider(ctx, name, db.NullProviderType{}, projectId, store)
 	if err != nil {
 		return db.Provider{}, err
 	}
 
@@ -77,11 +80,11 @@ func getProvidersByTrait(
 	store db.Store,
 	in HasProtoContext,
 	projectId uuid.UUID,
 	trait db.ProviderType,
 ) ([]db.Provider, error) {
-	name := getNameFilterParam(in.GetContext())
+	name := getNameFilterParam(in.GetContext().GetProvider())
 	t := db.NullProviderType{ProviderType: trait, Valid: true}
 	providers, err := findProvider(ctx, name, t, projectId, store)
 	if err != nil {
 		return nil, err
 	}
@@ -118,14 +121,14 @@ func findProvider(
 
 	return provs, nil
 }
 
 // getNameFilterParam allows us to build a name filter for our provider queries
-func getNameFilterParam(in *pb.Context) sql.NullString {
+func getNameFilterParam(providerName string) sql.NullString {
 	return sql.NullString{
-		String: in.GetProvider(),
-		Valid:  in.GetProvider() != "",
+		String: providerName,
+		Valid:  providerName != "",
 	}
 }
 
 // given a list of providers, inferProvider will validate the filter and
 // return the provider if it can be inferred. Note that this assumes that validation
diff --git a/internal/controlplane/handlers_artifacts.go b/internal/controlplane/handlers_artifacts.go
index 41301a6b6..edf349544 100644
--- a/internal/controlplane/handlers_artifacts.go
+++ b/internal/controlplane/handlers_artifacts.go
@@ -37,28 +37,24 @@ import (
 // ListArtifacts lists all artifacts for a given project and provider
 // nolint:gocyclo
 func (s *Server) ListArtifacts(ctx context.Context, in *pb.ListArtifactsRequest) (*pb.ListArtifactsResponse, error) {
 	entityCtx := engine.EntityFromContext(ctx)
 	projectID := entityCtx.Project.ID
-
-	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
-	if err != nil {
-		return nil, providerError(err)
-	}
+	providerName := entityCtx.Provider.Name
 
 	artifactFilter, err := parseArtifactListFrom(s.store, in.From)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse artifact list from: %w", err)
 	}
 
-	results, err := artifactFilter.listArtifacts(ctx, provider.Name, projectID)
+	results, err := artifactFilter.listArtifacts(ctx, providerName, projectID)
 	if err != nil {
 		return nil, fmt.Errorf("failed to list artifacts: %w", err)
 	}
 
 	// Telemetry logging
-	logger.BusinessRecord(ctx).Provider = provider.Name
+	logger.BusinessRecord(ctx).Provider = providerName
 	logger.BusinessRecord(ctx).Project = projectID
 
 	return &pb.ListArtifactsResponse{Results: results}, nil
 }
 
@@ -71,13 +67,14 @@ func (s *Server) GetArtifactByName(ctx context.Context, in *pb.GetArtifactByName
 		return nil, util.UserVisibleError(codes.InvalidArgument, "invalid artifact name user repoOwner/repoName/artifactName")
 	}
 
 	entityCtx := engine.EntityFromContext(ctx)
 	projectID := entityCtx.Project.ID
+	providerFilter := getNameFilterParam(entityCtx.Provider.Name)
 
 	repo, err := s.store.GetRepositoryByRepoName(ctx, db.GetRepositoryByRepoNameParams{
-		Provider:  in.GetContext().GetProvider(),
+		Provider:  providerFilter,
 		RepoOwner: nameParts[0],
 		RepoName:  nameParts[1],
 		ProjectID: projectID,
 	})
 	if err != nil {
@@ -239,12 +236,14 @@ func (filter *artifactListFilter) listArtifacts(ctx context.Context, provider st
 }
 
 func artifactListRepoFilter(
 	ctx context.Context, store db.Store, provider string, projectID uuid.UUID, repoSlubList []string,
 ) ([]*db.Repository, error) {
+	providerFilter := getNameFilterParam(provider)
+
 	repositories, err := store.ListRegisteredRepositoriesByProjectIDAndProvider(ctx,
-		db.ListRegisteredRepositoriesByProjectIDAndProviderParams{Provider: provider, ProjectID: projectID})
+		db.ListRegisteredRepositoriesByProjectIDAndProviderParams{Provider: providerFilter, ProjectID: projectID})
 	if err != nil {
 		if errors.Is(err, sql.ErrNoRows) {
 			return nil, status.Errorf(codes.NotFound, "repositories not found")
 		}
 		return nil, status.Errorf(codes.Unknown, "failed to get repositories: %s", err)
diff --git a/internal/controlplane/handlers_oauth.go b/internal/controlplane/handlers_oauth.go
index 1f3c3a196..891e84b1a 100644
--- a/internal/controlplane/handlers_oauth.go
+++ b/internal/controlplane/handlers_oauth.go
@@ -39,17 +39,14 @@ import (
 	mcrypto "github.com/stacklok/minder/internal/crypto"
 	"github.com/stacklok/minder/internal/db"
 	"github.com/stacklok/minder/internal/engine"
 	"github.com/stacklok/minder/internal/logger"
 	"github.com/stacklok/minder/internal/providers"
-	"github.com/stacklok/minder/internal/providers/github/oauth"
 	"github.com/stacklok/minder/internal/util"
 	pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
 )
 
-const defaultProvider = oauth.Github
-
 // GetAuthorizationURL returns the URL to redirect the user to for authorization
 // and the state to be used for the callback. It accepts a provider string
 // and a boolean indicating whether the client is a CLI or web client
 func (s *Server) GetAuthorizationURL(ctx context.Context,
 	req *pb.GetAuthorizationURLRequest) (*pb.GetAuthorizationURLResponse, error) {
@@ -382,12 +379,21 @@ func (s *Server) exchangeCodeForToken(ctx context.Context, providerName string,
 // StoreProviderToken stores the provider token for a project
 func (s *Server) StoreProviderToken(ctx context.Context,
 	in *pb.StoreProviderTokenRequest) (*pb.StoreProviderTokenResponse, error) {
 	entityCtx := engine.EntityFromContext(ctx)
 	projectID := entityCtx.Project.ID
+	providerName := entityCtx.Provider.Name
+
+	if providerName == "" {
+		return nil, status.Errorf(codes.InvalidArgument, "provider name is required")
+	}
 
-	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
+	provider, err := s.store.GetProviderByName(ctx, db.GetProviderByNameParams{
+		// We don't check parent projects here because subprojects should not update the credentials of their parent
+		Projects: []uuid.UUID{projectID},
+		Name:     providerName,
+	})
 	if err != nil {
 		return nil, providerError(err)
 	}
 
 	if !slices.Contains(provider.AuthFlows, db.AuthorizationFlowUserInput) {
@@ -448,12 +454,21 @@ func (s *Server) StoreProviderToken(ctx context.Context,
 // Deprecated: Use VerifyProviderCredential instead
 func (s *Server) VerifyProviderTokenFrom(ctx context.Context,
 	in *pb.VerifyProviderTokenFromRequest) (*pb.VerifyProviderTokenFromResponse, error) {
 	entityCtx := engine.EntityFromContext(ctx)
 	projectID := entityCtx.Project.ID
+	providerName := entityCtx.Provider.Name
 
-	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
+	if providerName == "" {
+		return nil, status.Errorf(codes.InvalidArgument, "provider name is required")
+	}
+
+	provider, err := s.store.GetProviderByName(ctx, db.GetProviderByNameParams{
+		// We don't check parent projects here because subprojects should not check the credentials of their parent
+		Projects: []uuid.UUID{projectID},
+		Name:     providerName,
+	})
 	if err != nil {
 		return nil, providerError(err)
 	}
 
 	// check if a token has been created since timestamp
diff --git a/internal/controlplane/handlers_profile.go b/internal/controlplane/handlers_profile.go
index 02ce23648..9e01fc0cc 100644
--- a/internal/controlplane/handlers_profile.go
+++ b/internal/controlplane/handlers_profile.go
@@ -56,11 +56,11 @@ func (s *Server) CreateProfile(ctx context.Context,
 	if err != nil {
 		return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
 	}
 
 	// TODO: This will be removed once we decouple providers from profiles
-	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, entityCtx.Project.ID)
+	provider, err := getProviderFromRequestOrDefault(ctx, s.store, entityCtx.Provider.Name, entityCtx.Project.ID)
 	if err != nil {
 		return nil, providerError(err)
 	}
 
 	newProfile, err := db.WithTransaction(s.store, func(qtx db.ExtendQuerier) (*minderv1.Profile, error) {
@@ -632,11 +632,11 @@ func (s *Server) UpdateProfile(ctx context.Context,
 	if err != nil {
 		return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
 	}
 
 	// TODO: This will be removed once we decouple providers from profiles
-	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, entityCtx.Project.ID)
+	provider, err := getProviderFromRequestOrDefault(ctx, s.store, entityCtx.Provider.Name, entityCtx.Project.ID)
 	if err != nil {
 		return nil, providerError(err)
 	}
 
 	updatedProfile, err := db.WithTransaction(s.store, func(qtx db.ExtendQuerier) (*minderv1.Profile, error) {
diff --git a/internal/controlplane/handlers_repositories.go b/internal/controlplane/handlers_repositories.go
index 0611a3a18..0bc1f53ff 100644
--- a/internal/controlplane/handlers_repositories.go
+++ b/internal/controlplane/handlers_repositories.go
@@ -16,10 +16,12 @@ package controlplane
 
 import (
 	"context"
 	"database/sql"
 	"errors"
+	"fmt"
+	"slices"
 	"strings"
 
 	"github.com/google/uuid"
 	"github.com/rs/zerolog"
 	"google.golang.org/grpc/codes"
@@ -49,14 +51,11 @@ const maxFetchLimit = 100
 func (s *Server) RegisterRepository(
 	ctx context.Context,
 	in *pb.RegisterRepositoryRequest,
 ) (*pb.RegisterRepositoryResponse, error) {
 	projectID := getProjectID(ctx)
-	provider, client, err := s.getProviderAndClient(ctx, projectID, in)
-	if err != nil {
-		return nil, err
-	}
+	providerName := getProviderName(ctx)
 
 	// Validate that the Repository struct in the request
 	githubRepo := in.GetRepository()
 	// If the repo owner is missing, GitHub will assume a default value based
 	// on the user's credentials. An explicit check for owner is left out to
@@ -70,11 +69,21 @@ func (s *Server) RegisterRepository(
 		Str("repoOwner", githubRepo.GetOwner()).
 		Str("projectID", projectID.String()).
 		Logger()
 	ctx = l.WithContext(ctx)
 
-	newRepo, err := s.repos.CreateRepository(ctx, client, provider, projectID, githubRepo.GetOwner(), githubRepo.GetName())
+	provider, err := s.inferProviderByOwner(ctx, githubRepo.GetOwner(), projectID, providerName)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, "cannot get provider: %v", err)
+	}
+
+	client, err := s.getClientForProvider(ctx, provider)
+	if err != nil {
+		return nil, err
+	}
+
+	newRepo, err := s.repos.CreateRepository(ctx, client, &provider, projectID, githubRepo.GetOwner(), githubRepo.GetName())
 	if err != nil {
 		if errors.Is(err, ghrepo.ErrPrivateRepoForbidden) {
 			return nil, util.UserVisibleError(codes.InvalidArgument, err.Error())
 		}
 		return nil, util.UserVisibleError(codes.Internal, "unable to register repository: %v", err)
@@ -95,23 +104,21 @@ func (s *Server) RegisterRepository(
 // repositories that are registered present in the minder database
 func (s *Server) ListRepositories(ctx context.Context,
 	in *pb.ListRepositoriesRequest) (*pb.ListRepositoriesResponse, error) {
 	entityCtx := engine.EntityFromContext(ctx)
 	projectID := entityCtx.Project.ID
+	providerName := entityCtx.Provider.Name
 
-	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
-	if err != nil {
-		return nil, providerError(err)
-	}
+	providerFilter := getNameFilterParam(providerName)
 
 	reqRepoCursor, err := cursorutil.NewRepoCursor(in.GetCursor())
 	if err != nil {
 		return nil, util.UserVisibleError(codes.InvalidArgument, err.Error())
 	}
 
 	repoId := sql.NullInt64{}
-	if reqRepoCursor.ProjectId == projectID.String() && reqRepoCursor.Provider == provider.Name {
+	if reqRepoCursor.ProjectId == projectID.String() && reqRepoCursor.Provider == providerName {
 		repoId = sql.NullInt64{Valid: true, Int64: reqRepoCursor.RepoId}
 	}
 
 	limit := sql.NullInt64{Valid: false, Int64: 0}
 	reqLimit := in.GetLimit()
@@ -121,11 +128,11 @@ func (s *Server) ListRepositories(ctx context.Context,
 		}
 		limit = sql.NullInt64{Valid: true, Int64: reqLimit + 1}
 	}
 
 	repos, err := s.store.ListRepositoriesByProjectID(ctx, db.ListRepositoriesByProjectIDParams{
-		Provider:  provider.Name,
+		Provider:  providerFilter,
 		ProjectID: projectID,
 		RepoID:    repoId,
 		Limit:     limit,
 	})
 
@@ -151,11 +158,11 @@ func (s *Server) ListRepositories(ctx context.Context,
 	var respRepoCursor *cursorutil.RepoCursor
 	if limit.Valid && int64(len(repos)) == limit.Int64 {
 		lastRepo := repos[len(repos)-1]
 		respRepoCursor = &cursorutil.RepoCursor{
 			ProjectId: projectID.String(),
-			Provider:  provider.Name,
+			Provider:  providerName,
 			RepoId:    lastRepo.RepoID,
 		}
 
 		// remove the (limit + 1)th element from the results
 		results = results[:len(results)-1]
@@ -163,11 +170,11 @@ func (s *Server) ListRepositories(ctx context.Context,
 
 	resp.Results = results
 	resp.Cursor = respRepoCursor.String()
 
 	// Telemetry logging
-	logger.BusinessRecord(ctx).Provider = provider.Name
+	logger.BusinessRecord(ctx).Provider = providerName
 	logger.BusinessRecord(ctx).Project = projectID
 
 	return &resp, nil
 }
 
@@ -218,18 +225,14 @@ func (s *Server) GetRepositoryByName(ctx context.Context,
 		return nil, util.UserVisibleError(codes.InvalidArgument, "invalid repository name, needs to have the format: owner/name")
 	}
 
 	entityCtx := engine.EntityFromContext(ctx)
 	projectID := entityCtx.Project.ID
-
-	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
-	if err != nil {
-		return nil, providerError(err)
-	}
+	providerFilter := getNameFilterParam(entityCtx.Provider.Name)
 
 	repo, err := s.store.GetRepositoryByRepoName(ctx, db.GetRepositoryByRepoNameParams{
-		Provider:  provider.Name,
+		Provider:  providerFilter,
 		RepoOwner: fragments[0],
 		RepoName:  fragments[1],
 		ProjectID: projectID,
 	})
 
@@ -262,12 +265,14 @@ func (s *Server) DeleteRepositoryById(
 	parsedRepositoryID, err := uuid.Parse(in.RepositoryId)
 	if err != nil {
 		return nil, util.UserVisibleError(codes.InvalidArgument, "invalid repository ID")
 	}
 
-	err = s.deleteRepository(ctx, in, func(client v1.GitHub, projectID uuid.UUID, _ string) error {
-		return s.repos.DeleteRepositoryByID(ctx, client, projectID, parsedRepositoryID)
+	projectID := getProjectID(ctx)
+
+	err = s.deleteRepository(ctx, func() (db.Repository, error) {
+		return s.repos.GetRepositoryById(ctx, parsedRepositoryID, projectID)
 	})
 	if err != nil {
 		return nil, err
 	}
 
@@ -286,12 +291,15 @@ func (s *Server) DeleteRepositoryByName(
 	fragments := strings.Split(in.Name, "/")
 	if len(fragments) != 2 {
 		return nil, util.UserVisibleError(codes.InvalidArgument, "invalid repository name, needs to have the format: owner/name")
 	}
 
-	err := s.deleteRepository(ctx, in, func(client v1.GitHub, projectID uuid.UUID, providerName string) error {
-		return s.repos.DeleteRepositoryByName(ctx, client, projectID, providerName, fragments[0], fragments[1])
+	projectID := getProjectID(ctx)
+	providerName := getProviderName(ctx)
+
+	err := s.deleteRepository(ctx, func() (db.Repository, error) {
+		return s.repos.GetRepositoryByName(ctx, fragments[0], fragments[1], projectID, providerName)
 	})
 	if err != nil {
 		return nil, err
 	}
 
@@ -399,62 +407,114 @@ func (s *Server) listRemoteRepositoriesForProvider(
 	}
 
 	return results, nil
 }
 
-// TODO: this probably can probably be used elsewhere
-// returns project ID and github client - this flow of code exists in multiple
-// places
-func (s *Server) getProviderAndClient(
+// covers the common logic for the two varieties of repo deletion
+func (s *Server) deleteRepository(
 	ctx context.Context,
-	projectID uuid.UUID,
-	request HasProtoContext,
-) (*db.Provider, v1.GitHub, error) {
-	provider, err := getProviderFromRequestOrDefault(ctx, s.store, request, projectID)
+	repoQueryMethod func() (db.Repository, error),
+) error {
+	projectID := getProjectID(ctx)
+
+	repo, err := repoQueryMethod()
+	if errors.Is(err, sql.ErrNoRows) {
+		return status.Errorf(codes.NotFound, "repository not found")
+	} else if err != nil {
+		return status.Errorf(codes.Internal, "unexpected error fetching repo: %v", err)
+	}
+
+	provider, err := s.findProviderByName(ctx, repo.Provider, projectID)
 	if err != nil {
-		return nil, nil, providerError(err)
+		return status.Errorf(codes.Internal, "cannot get provider: %v", err)
 	}
 
+	client, err := s.getClientForProvider(ctx, provider)
+	if err != nil {
+		return status.Errorf(codes.Internal, "cannot get client for provider: %v", err)
+	}
+
+	err = s.repos.DeleteRepository(ctx, client, &repo)
+	if err != nil {
+		return status.Errorf(codes.Internal, "unexpected error deleting repo: %v", err)
+	}
+	return nil
+}
+
+// inferProviderByOwner returns the provider to use for a given repo owner
+func (s *Server) inferProviderByOwner(ctx context.Context, owner string, projectID uuid.UUID, providerName string,
+) (db.Provider, error) {
+	if providerName != "" {
+		return s.findProviderByName(ctx, providerName, projectID)
+	}
+	opts, err := getProvidersByTrait(ctx, s.store, nil, projectID, db.ProviderTypeGithub)
+	if err != nil {
+		return db.Provider{}, fmt.Errorf("error getting providers: %v", err)
+	}
+
+	slices.SortFunc(opts, func(a, b db.Provider) int {
+		// Sort GitHub OAuth provider after all GitHub App providers
+		if a.Class.ProviderClass == db.ProviderClassGithub && b.Class.ProviderClass == db.ProviderClassGithubApp {
+			return 1
+		}
+		if a.Class.ProviderClass == db.ProviderClassGithubApp && b.Class.ProviderClass == db.ProviderClassGithub {
+			return -1
+		}
+		return 0
+	})
+
+	for _, prov := range opts {
+		if github.CanHandleOwner(ctx, prov, owner) {
+			return prov, nil
+		}
+	}
+
+	return db.Provider{}, fmt.Errorf("no providers can handle repo owned by %s", owner)
+}
+
+func (s *Server) getClientForProvider(
+	ctx context.Context,
+	provider db.Provider,
+) (v1.GitHub, error) {
 	pbOpts := []providers.ProviderBuilderOption{
 		providers.WithProviderMetrics(s.provMt),
 		providers.WithRestClientCache(s.restClientCache),
 	}
 
 	p, err := providers.GetProviderBuilder(ctx, provider, s.store, s.cryptoEngine, &s.cfg.Provider, pbOpts...)
 	if err != nil {
-		return nil, nil, status.Errorf(codes.Internal, "cannot get provider builder: %v", err)
+		return nil, status.Errorf(codes.Internal, "cannot get provider builder: %v", err)
 	}
 
 	client, err := p.GetGitHub()
 	if err != nil {
-		return nil, nil, status.Errorf(codes.Internal, "error creating github provider: %v", err)
+		return nil, status.Errorf(codes.Internal, "error creating github provider: %v", err)
 	}
 
-	return &provider, client, nil
+	return client, nil
 }
 
-// covers the common logic for the two varieties of repo deletion
-func (s *Server) deleteRepository(
-	ctx context.Context,
-	request HasProtoContext,
-	deletionMethod func(v1.GitHub, uuid.UUID, string) error,
-) error {
-	projectID := getProjectID(ctx)
-	provider, client, err := s.getProviderAndClient(ctx, projectID, request)
+func (s *Server) findProviderByName(ctx context.Context, providerName string, projectID uuid.UUID) (db.Provider, error) {
+	ph, err := s.store.GetParentProjects(ctx, projectID)
 	if err != nil {
-		return err
+		return db.Provider{}, fmt.Errorf("error getting project hierarchy: %v", err)
 	}
 
-	err = deletionMethod(client, projectID, provider.Name)
-	if errors.Is(err, sql.ErrNoRows) {
-		return status.Errorf(codes.NotFound, "repository not found")
-	} else if err != nil {
-		return status.Errorf(codes.Internal, "unexpected error deleting repo: %v", err)
+	provider, err := s.store.GetProviderByName(ctx, db.GetProviderByNameParams{
+		Name:     providerName,
+		Projects: ph,
+	})
+	if err != nil {
+		return db.Provider{}, fmt.Errorf("error getting provider: %v", err)
 	}
-
-	return nil
+	return provider, nil
 }
 
 func getProjectID(ctx context.Context) uuid.UUID {
 	entityCtx := engine.EntityFromContext(ctx)
 	return entityCtx.Project.ID
 }
+
+func getProviderName(ctx context.Context) string {
+	entityCtx := engine.EntityFromContext(ctx)
+	return entityCtx.Provider.Name
+}
diff --git a/internal/controlplane/handlers_repositories_test.go b/internal/controlplane/handlers_repositories_test.go
index efd72bc8d..60f8b5be2 100644
--- a/internal/controlplane/handlers_repositories_test.go
+++ b/internal/controlplane/handlers_repositories_test.go
@@ -53,11 +53,11 @@ func TestServer_RegisterRepository(t *testing.T) {
 		{
 			Name:          "Repo creation fails when provider cannot be found",
 			RepoOwner:     repoOwner,
 			RepoName:      repoName,
 			ProviderFails: true,
-			ExpectedError: "cannot retrieve providers",
+			ExpectedError: "error getting provider",
 		},
 		{
 			Name:          "Repo creation fails when repo name is missing",
 			RepoOwner:     repoOwner,
 			RepoName:      "",
@@ -143,14 +143,15 @@ func TestServer_DeleteRepository(t *testing.T) {
 		RepoServiceSetup repoMockBuilder
 		ProviderFails    bool
 		ExpectedError    string
 	}{
 		{
-			Name:          "deletion fails when provider cannot be found",
-			RepoName:      repoOwnerAndName,
-			ProviderFails: true,
-			ExpectedError: "cannot retrieve providers",
+			Name:             "deletion fails when provider cannot be found",
+			RepoName:         repoOwnerAndName,
+			RepoServiceSetup: newRepoService(withSuccessfulGetRepoByName),
+			ProviderFails:    true,
+			ExpectedError:    "error getting provider",
 		},
 		{
 			Name:          "delete by name fails when name is malformed",
 			RepoName:      "I am not a repo name",
 			ExpectedError: "invalid repository name",
@@ -161,34 +162,34 @@ func TestServer_DeleteRepository(t *testing.T) {
 			ExpectedError: "invalid repository ID",
 		},
 		{
 			Name:             "deletion fails when repo is not found",
 			RepoName:         repoOwnerAndName,
-			RepoServiceSetup: newRepoService(withFailedDeleteByName(sql.ErrNoRows)),
+			RepoServiceSetup: newRepoService(withFailedGetRepoByName(sql.ErrNoRows)),
 			ExpectedError:    "repository not found",
 		},
 		{
 			Name:             "deletion fails when repo service returns error",
 			RepoName:         repoOwnerAndName,
-			RepoServiceSetup: newRepoService(withFailedDeleteByName(errDefault)),
+			RepoServiceSetup: newRepoService(withSuccessfulGetRepoByName, withFailedDelete(errDefault)),
 			ExpectedError:    "unexpected error deleting repo",
 		},
 		{
 			Name:             "delete by ID fails when repo service returns error",
 			RepoID:           repoID,
-			RepoServiceSetup: newRepoService(withFailedDeleteByID(errDefault)),
+			RepoServiceSetup: newRepoService(withSuccessfulGetRepoById, withFailedDelete(errDefault)),
 			ExpectedError:    "unexpected error deleting repo",
 		},
 		{
 			Name:             "delete by name succeeds",
 			RepoName:         repoOwnerAndName,
-			RepoServiceSetup: newRepoService(withSuccessfulDeleteByName),
+			RepoServiceSetup: newRepoService(withSuccessfulGetRepoByName, withSuccessfulDelete),
 		},
 		{
 			Name:             "delete by ID succeeds",
 			RepoID:           repoID,
-			RepoServiceSetup: newRepoService(withSuccessfulDeleteByID),
+			RepoServiceSetup: newRepoService(withSuccessfulGetRepoById, withSuccessfulDelete),
 		},
 	}
 
 	for i := range scenarios {
 		scenario := scenarios[i]
@@ -289,39 +290,43 @@ func withFailedCreate(err error) func(repoServiceMock) {
 			CreateRepository(gomock.Any(), gomock.Any(), gomock.Any(), projectID, repoOwner, repoName).
 			Return(nil, err)
 	}
 }
 
-func withSuccessfulDeleteByName(mock repoServiceMock) {
-	withFailedDeleteByName(nil)(mock)
+func withSuccessfulDelete(mock repoServiceMock) {
+	withFailedDelete(nil)(mock)
 }
 
-func withFailedDeleteByName(err error) func(repoServiceMock) {
+func withFailedDelete(err error) func(repoServiceMock) {
 	return func(mock repoServiceMock) {
 		mock.EXPECT().
-			DeleteRepositoryByName(gomock.Any(), gomock.Any(), projectID, gomock.Any(), repoOwner, repoName).
+			DeleteRepository(gomock.Any(), gomock.Any(), gomock.Any()).
 			Return(err)
 	}
 }
 
-func withSuccessfulDeleteByID(mock repoServiceMock) {
-	withFailedDeleteByID(nil)(mock)
+func withSuccessfulGetRepoById(mock repoServiceMock) {
+	mock.EXPECT().
+		GetRepositoryById(gomock.Any(), gomock.Any(), gomock.Any()).
+		Return(db.Repository{}, nil)
 }
 
-func withFailedDeleteByID(err error) func(repoServiceMock) {
+func withSuccessfulGetRepoByName(mock repoServiceMock) {
+	mock.EXPECT().
+		GetRepositoryByName(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+		Return(db.Repository{}, nil)
+}
+
+func withFailedGetRepoByName(err error) func(repoServiceMock) {
 	return func(mock repoServiceMock) {
 		mock.EXPECT().
-			DeleteRepositoryByID(gomock.Any(), gomock.Any(), projectID, gomock.Any()).
-			Return(err)
+			GetRepositoryByName(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+			Return(db.Repository{}, err)
 	}
 }
 
-func createServer(
-	ctrl *gomock.Controller,
-	repoServiceSetup repoMockBuilder,
-	providerFails bool,
-) *Server {
+func createServer(ctrl *gomock.Controller, repoServiceSetup repoMockBuilder, providerFails bool) *Server {
 	var svc ghrepo.RepositoryService
 	if repoServiceSetup != nil {
 		svc = repoServiceSetup(ctrl)
 	}
 
@@ -346,16 +351,16 @@ func createServer(
 		Return([]uuid.UUID{projectID}, nil).
 		AnyTimes()
 
 	if providerFails {
 		store.EXPECT().
-			FindProviders(gomock.Any(), gomock.Any()).
-			Return(nil, errDefault)
+			GetProviderByName(gomock.Any(), gomock.Any()).
+			Return(db.Provider{}, errDefault)
 	} else {
 		store.EXPECT().
-			FindProviders(gomock.Any(), gomock.Any()).
-			Return([]db.Provider{provider}, nil).AnyTimes()
+			GetProviderByName(gomock.Any(), gomock.Any()).
+			Return(provider, nil).AnyTimes()
 		store.EXPECT().
 			GetAccessTokenByProjectID(gomock.Any(), gomock.Any()).
 			Return(db.ProviderAccessToken{
 				EncryptedToken: "encryptedToken",
 			}, nil).AnyTimes()
diff --git a/internal/db/repositories.sql.go b/internal/db/repositories.sql.go
index 3c63d9d05..703f2f2c7 100644
--- a/internal/db/repositories.sql.go
+++ b/internal/db/repositories.sql.go
@@ -201,26 +201,28 @@ func (q *Queries) GetRepositoryByRepoID(ctx context.Context, repoID int64) (Repo
 	)
 	return i, err
 }
 
 const getRepositoryByRepoName = `-- name: GetRepositoryByRepoName :one
-SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories WHERE provider = $1 AND repo_owner = $2 AND repo_name = $3 AND project_id = $4
+SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories
+    WHERE repo_owner = $1 AND repo_name = $2 AND project_id = $3
+    AND lower(provider) = lower($4::text) OR $4::text IS NULL
 `
 
 type GetRepositoryByRepoNameParams struct {
-	Provider  string    `json:"provider"`
-	RepoOwner string    `json:"repo_owner"`
-	RepoName  string    `json:"repo_name"`
-	ProjectID uuid.UUID `json:"project_id"`
+	RepoOwner string         `json:"repo_owner"`
+	RepoName  string         `json:"repo_name"`
+	ProjectID uuid.UUID      `json:"project_id"`
+	Provider  sql.NullString `json:"provider"`
 }
 
 func (q *Queries) GetRepositoryByRepoName(ctx context.Context, arg GetRepositoryByRepoNameParams) (Repository, error) {
 	row := q.db.QueryRowContext(ctx, getRepositoryByRepoName,
-		arg.Provider,
 		arg.RepoOwner,
 		arg.RepoName,
 		arg.ProjectID,
+		arg.Provider,
 	)
 	var i Repository
 	err := row.Scan(
 		&i.ID,
 		&i.Provider,
@@ -243,21 +245,22 @@ func (q *Queries) GetRepositoryByRepoName(ctx context.Context, arg GetRepository
 	return i, err
 }
 
 const listRegisteredRepositoriesByProjectIDAndProvider = `-- name: ListRegisteredRepositoriesByProjectIDAndProvider :many
 SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories
-WHERE provider = $1 AND project_id = $2 AND webhook_id IS NOT NULL
+WHERE project_id = $1 AND webhook_id IS NOT NULL
+    AND lower(provider) = lower($2::text) OR $2::text IS NULL
 ORDER BY repo_name
 `
 
 type ListRegisteredRepositoriesByProjectIDAndProviderParams struct {
-	Provider  string    `json:"provider"`
-	ProjectID uuid.UUID `json:"project_id"`
+	ProjectID uuid.UUID      `json:"project_id"`
+	Provider  sql.NullString `json:"provider"`
 }
 
 func (q *Queries) ListRegisteredRepositoriesByProjectIDAndProvider(ctx context.Context, arg ListRegisteredRepositoriesByProjectIDAndProviderParams) ([]Repository, error) {
-	rows, err := q.db.QueryContext(ctx, listRegisteredRepositoriesByProjectIDAndProvider, arg.Provider, arg.ProjectID)
+	rows, err := q.db.QueryContext(ctx, listRegisteredRepositoriesByProjectIDAndProvider, arg.ProjectID, arg.Provider)
 	if err != nil {
 		return nil, err
 	}
 	defer rows.Close()
 	items := []Repository{}
@@ -295,28 +298,29 @@ func (q *Queries) ListRegisteredRepositoriesByProjectIDAndProvider(ctx context.C
 	return items, nil
 }
 
 const listRepositoriesByProjectID = `-- name: ListRepositoriesByProjectID :many
 SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories
-WHERE provider = $1 AND project_id = $2
-  AND (repo_id >= $3 OR $3 IS NULL)
+WHERE project_id = $1
+  AND (repo_id >= $2 OR $2 IS NULL)
+  AND lower(provider) = lower(COALESCE($3, provider)::text)
 ORDER BY project_id, provider, repo_id
 LIMIT $4::bigint
 `
 
 type ListRepositoriesByProjectIDParams struct {
-	Provider  string        `json:"provider"`
-	ProjectID uuid.UUID     `json:"project_id"`
-	RepoID    sql.NullInt64 `json:"repo_id"`
-	Limit     sql.NullInt64 `json:"limit"`
+	ProjectID uuid.UUID      `json:"project_id"`
+	RepoID    sql.NullInt64  `json:"repo_id"`
+	Provider  sql.NullString `json:"provider"`
+	Limit     sql.NullInt64  `json:"limit"`
 }
 
 func (q *Queries) ListRepositoriesByProjectID(ctx context.Context, arg ListRepositoriesByProjectIDParams) ([]Repository, error) {
 	rows, err := q.db.QueryContext(ctx, listRepositoriesByProjectID,
-		arg.Provider,
 		arg.ProjectID,
 		arg.RepoID,
+		arg.Provider,
 		arg.Limit,
 	)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/db/repositories_test.go b/internal/db/repositories_test.go
index 6cc2ff55c..c5c8464f4 100644
--- a/internal/db/repositories_test.go
+++ b/internal/db/repositories_test.go
@@ -128,11 +128,14 @@ func TestGetRepositoryByRepoName(t *testing.T) {
 	project := createRandomProject(t, org.ID)
 	prov := createRandomProvider(t, project.ID)
 	repo1 := createRandomRepository(t, project.ID, prov)
 
 	repo2, err := testQueries.GetRepositoryByRepoName(context.Background(), GetRepositoryByRepoNameParams{
-		Provider:  repo1.Provider,
+		Provider: sql.NullString{
+			String: repo1.Provider,
+			Valid:  true,
+		},
 		RepoOwner: repo1.RepoOwner,
 		RepoName:  repo1.RepoName,
 		ProjectID: project.ID,
 	})
 	require.NoError(t, err)
@@ -166,11 +169,14 @@ func TestListRepositoriesByProjectID(t *testing.T) {
 			r.RepoID = int64(i)
 		})
 	}
 
 	arg := ListRepositoriesByProjectIDParams{
-		Provider:  prov.Name,
+		Provider: sql.NullString{
+			String: prov.Name,
+			Valid:  true,
+		},
 		ProjectID: project.ID,
 	}
 
 	repos, err := testQueries.ListRepositoriesByProjectID(context.Background(), arg)
 	require.NoError(t, err)
diff --git a/internal/providers/github/common.go b/internal/providers/github/common.go
index 49a7a52ce..396bc4a7e 100644
--- a/internal/providers/github/common.go
+++ b/internal/providers/github/common.go
@@ -812,5 +812,18 @@ func IsMinderHook(hook *github.Hook, hostURL string) (bool, error) {
 		return true, nil
 	}
 
 	return false, nil
 }
+
+// CanHandleOwner checks if the GitHub provider has the right credentials to handle the owner
+func CanHandleOwner(_ context.Context, prov db.Provider, owner string) bool {
+	// TODO: this is fragile and does not handle organization renames, in the future we can make sure the credential
+	// has admin permissions on the owner
+	if prov.Name == fmt.Sprintf("%s-%s", db.ProviderClassGithubApp, owner) {
+		return true
+	}
+	if prov.Class.ProviderClass == db.ProviderClassGithub {
+		return true
+	}
+	return false
+}
diff --git a/internal/reconcilers/run_profile.go b/internal/reconcilers/run_profile.go
index de3a15d19..b3b8c2215 100644
--- a/internal/reconcilers/run_profile.go
+++ b/internal/reconcilers/run_profile.go
@@ -125,11 +125,14 @@ func (r *Reconciler) publishProfileInitEvents(
 	ctx context.Context,
 	ectx *engine.EntityContext,
 ) error {
 	dbrepos, err := r.store.ListRegisteredRepositoriesByProjectIDAndProvider(ctx,
 		db.ListRegisteredRepositoriesByProjectIDAndProviderParams{
-			Provider:  ectx.Provider.Name,
+			Provider: sql.NullString{
+				String: ectx.Provider.Name,
+				Valid:  ectx.Provider.Name != "",
+			},
 			ProjectID: ectx.Project.ID,
 		})
 	if err != nil {
 		return fmt.Errorf("publishProfileInitEvents: error getting registered repos: %v", err)
 	}
diff --git a/internal/repositories/github/mock/service.go b/internal/repositories/github/mock/service.go
index 345e52082..da45274a9 100644
--- a/internal/repositories/github/mock/service.go
+++ b/internal/repositories/github/mock/service.go
@@ -56,32 +56,48 @@ func (m *MockRepositoryService) CreateRepository(arg0 context.Context, arg1 clie
 func (mr *MockRepositoryServiceMockRecorder) CreateRepository(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRepository", reflect.TypeOf((*MockRepositoryService)(nil).CreateRepository), arg0, arg1, arg2, arg3, arg4, arg5)
 }
 
-// DeleteRepositoryByID mocks base method.
-func (m *MockRepositoryService) DeleteRepositoryByID(arg0 context.Context, arg1 clients.GitHubRepoClient, arg2, arg3 uuid.UUID) error {
+// DeleteRepository mocks base method.
+func (m *MockRepositoryService) DeleteRepository(arg0 context.Context, arg1 clients.GitHubRepoClient, arg2 *db.Repository) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "DeleteRepositoryByID", arg0, arg1, arg2, arg3)
+	ret := m.ctrl.Call(m, "DeleteRepository", arg0, arg1, arg2)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// DeleteRepositoryByID indicates an expected call of DeleteRepositoryByID.
-func (mr *MockRepositoryServiceMockRecorder) DeleteRepositoryByID(arg0, arg1, arg2, arg3 any) *gomock.Call {
+// DeleteRepository indicates an expected call of DeleteRepository.
+func (mr *MockRepositoryServiceMockRecorder) DeleteRepository(arg0, arg1, arg2 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryByID", reflect.TypeOf((*MockRepositoryService)(nil).DeleteRepositoryByID), arg0, arg1, arg2, arg3)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepository", reflect.TypeOf((*MockRepositoryService)(nil).DeleteRepository), arg0, arg1, arg2)
 }
 
-// DeleteRepositoryByName mocks base method.
-func (m *MockRepositoryService) DeleteRepositoryByName(arg0 context.Context, arg1 clients.GitHubRepoClient, arg2 uuid.UUID, arg3, arg4, arg5 string) error {
+// GetRepositoryById mocks base method.
+func (m *MockRepositoryService) GetRepositoryById(arg0 context.Context, arg1, arg2 uuid.UUID) (db.Repository, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "DeleteRepositoryByName", arg0, arg1, arg2, arg3, arg4, arg5)
-	ret0, _ := ret[0].(error)
-	return ret0
+	ret := m.ctrl.Call(m, "GetRepositoryById", arg0, arg1, arg2)
+	ret0, _ := ret[0].(db.Repository)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetRepositoryById indicates an expected call of GetRepositoryById.
+func (mr *MockRepositoryServiceMockRecorder) GetRepositoryById(arg0, arg1, arg2 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryById", reflect.TypeOf((*MockRepositoryService)(nil).GetRepositoryById), arg0, arg1, arg2)
+}
+
+// GetRepositoryByName mocks base method.
+func (m *MockRepositoryService) GetRepositoryByName(arg0 context.Context, arg1, arg2 string, arg3 uuid.UUID, arg4 string) (db.Repository, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetRepositoryByName", arg0, arg1, arg2, arg3, arg4)
+	ret0, _ := ret[0].(db.Repository)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
-// DeleteRepositoryByName indicates an expected call of DeleteRepositoryByName.
-func (mr *MockRepositoryServiceMockRecorder) DeleteRepositoryByName(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomock.Call {
+// GetRepositoryByName indicates an expected call of GetRepositoryByName.
+func (mr *MockRepositoryServiceMockRecorder) GetRepositoryByName(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryByName", reflect.TypeOf((*MockRepositoryService)(nil).DeleteRepositoryByName), arg0, arg1, arg2, arg3, arg4, arg5)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryByName", reflect.TypeOf((*MockRepositoryService)(nil).GetRepositoryByName), arg0, arg1, arg2, arg3, arg4)
 }
diff --git a/internal/repositories/github/service.go b/internal/repositories/github/service.go
index ccb2571e3..dab648ac5 100644
--- a/internal/repositories/github/service.go
+++ b/internal/repositories/github/service.go
@@ -47,28 +47,26 @@ type RepositoryService interface {
 		provider *db.Provider,
 		projectID uuid.UUID,
 		repoName string,
 		repoOwner string,
 	) (*pb.Repository, error)
-	// DeleteRepositoryByName removes the webhook and deletes the repo from the
-	// database. The repo is identified by its name and project.
-	DeleteRepositoryByName(
+	// DeleteRepository removes the webhook and deletes the repo from the database.
+	DeleteRepository(
 		ctx context.Context,
 		client ghclient.GitHubRepoClient,
-		projectID uuid.UUID,
-		providerName string,
-		repoOwner string,
-		repoName string,
+		repo *db.Repository,
 	) error
-	// DeleteRepositoryByID removes the webhook and deletes the repo from the
-	// database. The repo is identified by its database ID and project.
-	DeleteRepositoryByID(
+	// GetRepositoryById retrieves a repository by its ID and project.
+	GetRepositoryById(ctx context.Context, repositoryID uuid.UUID, projectID uuid.UUID) (db.Repository, error)
+	// GetRepositoryByName retrieves a repository by its name, owner, project and provider (if specified).
+	GetRepositoryByName(
 		ctx context.Context,
-		client ghclient.GitHubRepoClient,
+		repoOwner string,
+		repoName string,
 		projectID uuid.UUID,
-		repoID uuid.UUID,
-	) error
+		providerName string,
+	) (db.Repository, error)
 }
 
 var (
 	// ErrPrivateRepoForbidden is returned when creation fails due to an
 	// attempt to register a private repo in a project which does not allow
@@ -153,49 +151,42 @@ func (r *repositoryService) CreateRepository(
 	logger.BusinessRecord(ctx).Repository = dbID
 
 	return pbRepo, nil
 }
 
-func (r *repositoryService) DeleteRepositoryByName(
+func (r *repositoryService) GetRepositoryById(
 	ctx context.Context,
-	client ghclient.GitHubRepoClient,
+	repositoryID uuid.UUID,
 	projectID uuid.UUID,
-	providerName string,
-	repoOwner string,
-	repoName string,
-) error {
-	params := db.GetRepositoryByRepoNameParams{
-		Provider:  providerName,
-		RepoOwner: repoOwner,
-		RepoName:  repoName,
+) (db.Repository, error) {
+	return r.store.GetRepositoryByIDAndProject(ctx, db.GetRepositoryByIDAndProjectParams{
+		ID:        repositoryID,
 		ProjectID: projectID,
-	}
-	repo, err := r.store.GetRepositoryByRepoName(ctx, params)
-	if err != nil {
-		return err
-	}
-	return r.deleteRepository(ctx, client, &repo)
+	})
 }
 
-func (r *repositoryService) DeleteRepositoryByID(
+func (r *repositoryService) GetRepositoryByName(
 	ctx context.Context,
-	client ghclient.GitHubRepoClient,
+	repoOwner string,
+	repoName string,
 	projectID uuid.UUID,
-	repoID uuid.UUID,
-) error {
-	repo, err := r.store.GetRepositoryByIDAndProject(ctx, db.GetRepositoryByIDAndProjectParams{
-		ID:        repoID,
+	providerName string,
+) (db.Repository, error) {
+	providerFilter := sql.NullString{
+		String: providerName,
+		Valid:  providerName != "",
+	}
+	params := db.GetRepositoryByRepoNameParams{
+		Provider:  providerFilter,
+		RepoOwner: repoOwner,
+		RepoName:  repoName,
 		ProjectID: projectID,
-	})
-	if err != nil {
-		log.Printf("error is %v", err)
-		return err
 	}
-	return r.deleteRepository(ctx, client, &repo)
+	return r.store.GetRepositoryByRepoName(ctx, params)
 }
 
-func (r *repositoryService) deleteRepository(ctx context.Context, client ghclient.GitHubRepoClient, repo *db.Repository) error {
+func (r *repositoryService) DeleteRepository(ctx context.Context, client ghclient.GitHubRepoClient, repo *db.Repository) error {
 	var err error
 
 	// Cleanup any webhook we created for this project
 	// `webhooksManager.DeleteWebhook` is idempotent, so there are no issues
 	// re-running this if we have to retry the request.
diff --git a/internal/repositories/github/service_test.go b/internal/repositories/github/service_test.go
index 7c334822e..6bbd6ce61 100644
--- a/internal/repositories/github/service_test.go
+++ b/internal/repositories/github/service_test.go
@@ -140,55 +140,66 @@ func TestRepositoryService_DeleteRepository(t *testing.T) {
 		WebhookSetup  whMockBuilder
 		DeleteType    DeleteCallType
 		ShouldSucceed bool
 	}{
 		{
-			Name:       "Delete by ID fails when repo does not exist in DB",
-			DBSetup:    newDBMock(withNotFoundByID),
-			DeleteType: ByID,
-		},
-		{
-			Name:       "Delete by Name fails when repo does not exist in DB",
-			DBSetup:    newDBMock(withNotFoundByName),
-			DeleteType: ByName,
-		},
-		{
-			Name:         "Delete by ID fails when webhook cannot be deleted",
-			DBSetup:      newDBMock(withRepoByID),
-			WebhookSetup: newWebhookMock(withFailedWebhookDelete),
-			DeleteType:   ByID,
-		},
-		{
-			Name:         "Delete by Name fails when webhook cannot be deleted",
-			DBSetup:      newDBMock(withRepoByName),
+			Name:         "Delete fails when webhook cannot be deleted",
 			WebhookSetup: newWebhookMock(withFailedWebhookDelete),
-			DeleteType:   ByName,
 		},
 		{
 			Name:         "Delete by ID fails when repo cannot be deleted from DB",
-			DBSetup:      newDBMock(withRepoByID, withFailedDelete),
+			DBSetup:      newDBMock(withFailedDelete),
 			WebhookSetup: newWebhookMock(withSuccessfulWebhookDelete),
-			DeleteType:   ByID,
 		},
 		{
-			Name:         "Delete by Name fails when repo cannot be deleted from DB",
-			DBSetup:      newDBMock(withRepoByName, withFailedDelete),
-			WebhookSetup: newWebhookMock(withSuccessfulWebhookDelete),
-			DeleteType:   ByName,
-		},
-		{
-			Name:          "Delete by ID succeeds",
-			DBSetup:       newDBMock(withRepoByID, withSuccessfulDelete),
+			Name:          "Delete succeeds",
+			DBSetup:       newDBMock(withSuccessfulDelete),
 			WebhookSetup:  newWebhookMock(withSuccessfulWebhookDelete),
-			DeleteType:    ByID,
 			ShouldSucceed: true,
 		},
+	}
+
+	for i := range scenarios {
+		scenario := scenarios[i]
+		t.Run(scenario.Name, func(t *testing.T) {
+			t.Parallel()
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+			ctx := context.Background()
+
+			// For the purposes of this test, we do not need to attach any
+			// mock behaviour to the client. We can leave it as a nil pointer.
+			var ghClient clients.GitHubRepoClient
+
+			svc := createService(ctrl, scenario.WebhookSetup, scenario.DBSetup, false)
+			err := svc.DeleteRepository(ctx, ghClient, &dbRepo)
+
+			if scenario.ShouldSucceed {
+				require.NoError(t, err)
+			} else {
+				require.Error(t, err)
+			}
+		})
+	}
+}
+
+func TestRepositoryService_GetRepositoryById(t *testing.T) {
+	t.Parallel()
+
+	scenarios := []struct {
+		Name          string
+		DBSetup       dbMockBuilder
+		DeleteType    DeleteCallType
+		ShouldSucceed bool
+	}{
+		{
+			Name:    "Get by ID fails when DB call fails",
+			DBSetup: newDBMock(withFailedGetById),
+		},
 		{
-			Name:          "Delete by Name succeeds",
-			DBSetup:       newDBMock(withRepoByName, withSuccessfulDelete),
-			WebhookSetup:  newWebhookMock(withSuccessfulWebhookDelete),
-			DeleteType:    ByName,
+			Name:          "Get by ID succeeds",
+			DBSetup:       newDBMock(withSuccessfulGetById),
 			ShouldSucceed: true,
 		},
 	}
 
 	for i := range scenarios {
@@ -199,22 +210,56 @@ func TestRepositoryService_DeleteRepository(t *testing.T) {
 			defer ctrl.Finish()
 			ctx := context.Background()
 
 			// For the purposes of this test, we do not need to attach any
 			// mock behaviour to the client. We can leave it as a nil pointer.
-			var ghClient clients.GitHubRepoClient
 
-			svc := createService(ctrl, scenario.WebhookSetup, scenario.DBSetup, false)
-			var err error
-			switch scenario.DeleteType {
-			case ByID:
-				err = svc.DeleteRepositoryByID(ctx, ghClient, projectID, repoID)
-			case ByName:
-				err = svc.DeleteRepositoryByName(ctx, ghClient, projectID, providerName, repoOwner, repoName)
-			default:
-				t.Fatalf("Unknown deletion type: %d", scenario.DeleteType)
+			svc := createService(ctrl, newWebhookMock(), scenario.DBSetup, false)
+			_, err := svc.GetRepositoryById(ctx, repoID, projectID)
+
+			if scenario.ShouldSucceed {
+				require.NoError(t, err)
+			} else {
+				require.Error(t, err)
 			}
+		})
+	}
+}
+
+func TestRepositoryService_GetRepositoryByName(t *testing.T) {
+	t.Parallel()
+
+	scenarios := []struct {
+		Name          string
+		DBSetup       dbMockBuilder
+		DeleteType    DeleteCallType
+		ShouldSucceed bool
+	}{
+		{
+			Name:    "Get by name fails when DB call fails",
+			DBSetup: newDBMock(withFailedGetByName),
+		},
+		{
+			Name:          "Get by name succeeds",
+			DBSetup:       newDBMock(withSuccessfulGetByName),
+			ShouldSucceed: true,
+		},
+	}
+
+	for i := range scenarios {
+		scenario := scenarios[i]
+		t.Run(scenario.Name, func(t *testing.T) {
+			t.Parallel()
+			ctrl := gomock.NewController(t)
+			defer ctrl.Finish()
+			ctx := context.Background()
+
+			// For the purposes of this test, we do not need to attach any
+			// mock behaviour to the client. We can leave it as a nil pointer.
+
+			svc := createService(ctrl, newWebhookMock(), scenario.DBSetup, false)
+			_, err := svc.GetRepositoryByName(ctx, repoOwner, repoName, projectID, providerName)
 
 			if scenario.ShouldSucceed {
 				require.NoError(t, err)
 			} else {
 				require.Error(t, err)
@@ -344,45 +389,44 @@ func withFailedWebhookDelete(mock whMock) {
 	mock.EXPECT().
 		DeleteWebhook(gomock.Any(), gomock.Any(), repoOwner, repoName, cf.HookID).
 		Return(errDefault)
 }
 
-func withNotFoundByID(mock dbMock) {
+func withFailedDelete(mock dbMock) {
 	mock.EXPECT().
-		GetRepositoryByIDAndProject(gomock.Any(), gomock.Any()).
-		Return(db.Repository{}, errDefault)
+		DeleteRepository(gomock.Any(), gomock.Eq(repoID)).
+		Return(errDefault)
 }
 
-func withNotFoundByName(mock dbMock) {
+func withSuccessfulDelete(mock dbMock) {
 	mock.EXPECT().
-		// TODO: do we want more strict assertions here?
-		GetRepositoryByRepoName(gomock.Any(), gomock.Any()).
-		Return(db.Repository{}, errDefault)
+		DeleteRepository(gomock.Any(), gomock.Eq(repoID)).
+		Return(nil)
 }
 
-func withRepoByID(mock dbMock) {
+func withFailedGetById(mock dbMock) {
 	mock.EXPECT().
 		GetRepositoryByIDAndProject(gomock.Any(), gomock.Any()).
-		Return(dbRepo, nil)
+		Return(db.Repository{}, errDefault)
 }
 
-func withRepoByName(mock dbMock) {
+func withSuccessfulGetById(mock dbMock) {
 	mock.EXPECT().
-		GetRepositoryByRepoName(gomock.Any(), gomock.Any()).
+		GetRepositoryByIDAndProject(gomock.Any(), gomock.Any()).
 		Return(dbRepo, nil)
 }
 
-func withFailedDelete(mock dbMock) {
+func withFailedGetByName(mock dbMock) {
 	mock.EXPECT().
-		DeleteRepository(gomock.Any(), gomock.Eq(repoID)).
-		Return(errDefault)
+		GetRepositoryByRepoName(gomock.Any(), gomock.Any()).
+		Return(db.Repository{}, errDefault)
 }
 
-func withSuccessfulDelete(mock dbMock) {
+func withSuccessfulGetByName(mock dbMock) {
 	mock.EXPECT().
-		DeleteRepository(gomock.Any(), gomock.Eq(repoID)).
-		Return(nil)
+		GetRepositoryByRepoName(gomock.Any(), gomock.Any()).
+		Return(dbRepo, nil)
 }
 
 func withFailedCreate(mock dbMock) {
 	mock.EXPECT().
 		CreateRepository(gomock.Any(), gomock.Any()).

Vulnerable code: 	ghclient "github.com/stacklok/minder/internal/providers/github/oauth"
	ArtifactCmd.PersistentFlags().StringP("provider", "p", ghclient.Github, "Name of the provider, i.e. github")
	ghclient "github.com/stacklok/minder/internal/providers/github/oauth"
	RepoCmd.PersistentFlags().StringP("provider", "p", ghclient.Github, "Name of the provider, i.e. github")
			Provider:  provider.Name,
SELECT * FROM repositories WHERE provider = $1 AND repo_owner = $2 AND repo_name = $3 AND project_id = $4;
WHERE provider = $1 AND project_id = $2
WHERE provider = $1 AND project_id = $2 AND webhook_id IS NOT NULL
	in HasProtoContext,
	name := getNameFilterParam(in.GetContext())
	name := getNameFilterParam(in.GetContext())
func getNameFilterParam(in *pb.Context) sql.NullString {
		String: in.GetProvider(),
		Valid:  in.GetProvider() != "",

	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
	if err != nil {
		return nil, providerError(err)
	}
	results, err := artifactFilter.listArtifacts(ctx, provider.Name, projectID)
	logger.BusinessRecord(ctx).Provider = provider.Name
		Provider:  in.GetContext().GetProvider(),
		db.ListRegisteredRepositoriesByProjectIDAndProviderParams{Provider: provider, ProjectID: projectID})
	"github.com/stacklok/minder/internal/providers/github/oauth"
const defaultProvider = oauth.Github

	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, entityCtx.Project.ID)
	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, entityCtx.Project.ID)
	provider, client, err := s.getProviderAndClient(ctx, projectID, in)
	if err != nil {
		return nil, err
	}
	newRepo, err := s.repos.CreateRepository(ctx, client, provider, projectID, githubRepo.GetOwner(), githubRepo.GetName())
	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
	if err != nil {
		return nil, providerError(err)
	}
	if reqRepoCursor.ProjectId == projectID.String() && reqRepoCursor.Provider == provider.Name {
		Provider:  provider.Name,
			Provider:  provider.Name,
	logger.BusinessRecord(ctx).Provider = provider.Name

	provider, err := getProviderFromRequestOrDefault(ctx, s.store, in, projectID)
	if err != nil {
		return nil, providerError(err)
	}
		Provider:  provider.Name,
	err = s.deleteRepository(ctx, in, func(client v1.GitHub, projectID uuid.UUID, _ string) error {
		return s.repos.DeleteRepositoryByID(ctx, client, projectID, parsedRepositoryID)
	err := s.deleteRepository(ctx, in, func(client v1.GitHub, projectID uuid.UUID, providerName string) error {
		return s.repos.DeleteRepositoryByName(ctx, client, projectID, providerName, fragments[0], fragments[1])
// TODO: this probably can probably be used elsewhere
// returns project ID and github client - this flow of code exists in multiple
// places
func (s *Server) getProviderAndClient(
	projectID uuid.UUID,
	request HasProtoContext,
) (*db.Provider, v1.GitHub, error) {
	provider, err := getProviderFromRequestOrDefault(ctx, s.store, request, projectID)
		return nil, nil, providerError(err)
		return nil, nil, status.Errorf(codes.Internal, "cannot get provider builder: %v", err)
		return nil, nil, status.Errorf(codes.Internal, "error creating github provider: %v", err)
	return &provider, client, nil
// covers the common logic for the two varieties of repo deletion
func (s *Server) deleteRepository(
	ctx context.Context,
	request HasProtoContext,
	deletionMethod func(v1.GitHub, uuid.UUID, string) error,
) error {
	projectID := getProjectID(ctx)
	provider, client, err := s.getProviderAndClient(ctx, projectID, request)
		return err
	err = deletionMethod(client, projectID, provider.Name)
	if errors.Is(err, sql.ErrNoRows) {
		return status.Errorf(codes.NotFound, "repository not found")
	} else if err != nil {
		return status.Errorf(codes.Internal, "unexpected error deleting repo: %v", err)

	return nil
			ExpectedError: "cannot retrieve providers",
			Name:          "deletion fails when provider cannot be found",
			RepoName:      repoOwnerAndName,
			ProviderFails: true,
			ExpectedError: "cannot retrieve providers",
			RepoServiceSetup: newRepoService(withFailedDeleteByName(sql.ErrNoRows)),
			RepoServiceSetup: newRepoService(withFailedDeleteByName(errDefault)),
			RepoServiceSetup: newRepoService(withFailedDeleteByID(errDefault)),
			RepoServiceSetup: newRepoService(withSuccessfulDeleteByName),
			RepoServiceSetup: newRepoService(withSuccessfulDeleteByID),
func withSuccessfulDeleteByName(mock repoServiceMock) {
	withFailedDeleteByName(nil)(mock)
func withFailedDeleteByName(err error) func(repoServiceMock) {
			DeleteRepositoryByName(gomock.Any(), gomock.Any(), projectID, gomock.Any(), repoOwner, repoName).
func withSuccessfulDeleteByID(mock repoServiceMock) {
	withFailedDeleteByID(nil)(mock)
func withFailedDeleteByID(err error) func(repoServiceMock) {
			DeleteRepositoryByID(gomock.Any(), gomock.Any(), projectID, gomock.Any()).
			Return(err)
func createServer(
	ctrl *gomock.Controller,
	repoServiceSetup repoMockBuilder,
	providerFails bool,
) *Server {
			FindProviders(gomock.Any(), gomock.Any()).
			Return(nil, errDefault)
			FindProviders(gomock.Any(), gomock.Any()).
			Return([]db.Provider{provider}, nil).AnyTimes()
SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories WHERE provider = $1 AND repo_owner = $2 AND repo_name = $3 AND project_id = $4
	Provider  string    `json:"provider"`
	RepoOwner string    `json:"repo_owner"`
	RepoName  string    `json:"repo_name"`
	ProjectID uuid.UUID `json:"project_id"`
		arg.Provider,
WHERE provider = $1 AND project_id = $2 AND webhook_id IS NOT NULL
	Provider  string    `json:"provider"`
	ProjectID uuid.UUID `json:"project_id"`
	rows, err := q.db.QueryContext(ctx, listRegisteredRepositoriesByProjectIDAndProvider, arg.Provider, arg.ProjectID)
WHERE provider = $1 AND project_id = $2
  AND (repo_id >= $3 OR $3 IS NULL)
	Provider  string        `json:"provider"`
	ProjectID uuid.UUID     `json:"project_id"`
	RepoID    sql.NullInt64 `json:"repo_id"`
	Limit     sql.NullInt64 `json:"limit"`
		arg.Provider,
		Provider:  repo1.Provider,
		Provider:  prov.Name,
			Provider:  ectx.Provider.Name,
// DeleteRepositoryByID mocks base method.
func (m *MockRepositoryService) DeleteRepositoryByID(arg0 context.Context, arg1 clients.GitHubRepoClient, arg2, arg3 uuid.UUID) error {
	ret := m.ctrl.Call(m, "DeleteRepositoryByID", arg0, arg1, arg2, arg3)
// DeleteRepositoryByID indicates an expected call of DeleteRepositoryByID.
func (mr *MockRepositoryServiceMockRecorder) DeleteRepositoryByID(arg0, arg1, arg2, arg3 any) *gomock.Call {
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryByID", reflect.TypeOf((*MockRepositoryService)(nil).DeleteRepositoryByID), arg0, arg1, arg2, arg3)
// DeleteRepositoryByName mocks base method.
func (m *MockRepositoryService) DeleteRepositoryByName(arg0 context.Context, arg1 clients.GitHubRepoClient, arg2 uuid.UUID, arg3, arg4, arg5 string) error {
	ret := m.ctrl.Call(m, "DeleteRepositoryByName", arg0, arg1, arg2, arg3, arg4, arg5)
	ret0, _ := ret[0].(error)
	return ret0
// DeleteRepositoryByName indicates an expected call of DeleteRepositoryByName.
func (mr *MockRepositoryServiceMockRecorder) DeleteRepositoryByName(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomock.Call {
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepositoryByName", reflect.TypeOf((*MockRepositoryService)(nil).DeleteRepositoryByName), arg0, arg1, arg2, arg3, arg4, arg5)
	// DeleteRepositoryByName removes the webhook and deletes the repo from the
	// database. The repo is identified by its name and project.
	DeleteRepositoryByName(
		projectID uuid.UUID,
		providerName string,
		repoOwner string,
		repoName string,
	// DeleteRepositoryByID removes the webhook and deletes the repo from the
	// database. The repo is identified by its database ID and project.
	DeleteRepositoryByID(
		client ghclient.GitHubRepoClient,
		repoID uuid.UUID,
	) error
func (r *repositoryService) DeleteRepositoryByName(
	client ghclient.GitHubRepoClient,
	providerName string,
	repoOwner string,
	repoName string,
) error {
	params := db.GetRepositoryByRepoNameParams{
		Provider:  providerName,
		RepoOwner: repoOwner,
		RepoName:  repoName,
	}
	repo, err := r.store.GetRepositoryByRepoName(ctx, params)
	if err != nil {
		return err
	}
	return r.deleteRepository(ctx, client, &repo)
func (r *repositoryService) DeleteRepositoryByID(
	client ghclient.GitHubRepoClient,
	repoID uuid.UUID,
) error {
	repo, err := r.store.GetRepositoryByIDAndProject(ctx, db.GetRepositoryByIDAndProjectParams{
		ID:        repoID,
	})
	if err != nil {
		log.Printf("error is %v", err)
		return err
	return r.deleteRepository(ctx, client, &repo)
func (r *repositoryService) deleteRepository(ctx context.Context, client ghclient.GitHubRepoClient, repo *db.Repository) error {
			Name:       "Delete by ID fails when repo does not exist in DB",
			DBSetup:    newDBMock(withNotFoundByID),
			DeleteType: ByID,
		},
		{
			Name:       "Delete by Name fails when repo does not exist in DB",
			DBSetup:    newDBMock(withNotFoundByName),
			DeleteType: ByName,
		},
		{
			Name:         "Delete by ID fails when webhook cannot be deleted",
			DBSetup:      newDBMock(withRepoByID),
			WebhookSetup: newWebhookMock(withFailedWebhookDelete),
			DeleteType:   ByID,
		},
		{
			Name:         "Delete by Name fails when webhook cannot be deleted",
			DBSetup:      newDBMock(withRepoByName),
			DeleteType:   ByName,
			DBSetup:      newDBMock(withRepoByID, withFailedDelete),
			DeleteType:   ByID,
			Name:         "Delete by Name fails when repo cannot be deleted from DB",
			DBSetup:      newDBMock(withRepoByName, withFailedDelete),
			WebhookSetup: newWebhookMock(withSuccessfulWebhookDelete),
			DeleteType:   ByName,
		},
		{
			Name:          "Delete by ID succeeds",
			DBSetup:       newDBMock(withRepoByID, withSuccessfulDelete),
			DeleteType:    ByID,
			Name:          "Delete by Name succeeds",
			DBSetup:       newDBMock(withRepoByName, withSuccessfulDelete),
			WebhookSetup:  newWebhookMock(withSuccessfulWebhookDelete),
			DeleteType:    ByName,
			var ghClient clients.GitHubRepoClient
			svc := createService(ctrl, scenario.WebhookSetup, scenario.DBSetup, false)
			var err error
			switch scenario.DeleteType {
			case ByID:
				err = svc.DeleteRepositoryByID(ctx, ghClient, projectID, repoID)
			case ByName:
				err = svc.DeleteRepositoryByName(ctx, ghClient, projectID, providerName, repoOwner, repoName)
			default:
				t.Fatalf("Unknown deletion type: %d", scenario.DeleteType)
func withNotFoundByID(mock dbMock) {
		GetRepositoryByIDAndProject(gomock.Any(), gomock.Any()).
		Return(db.Repository{}, errDefault)
func withNotFoundByName(mock dbMock) {
		// TODO: do we want more strict assertions here?
		GetRepositoryByRepoName(gomock.Any(), gomock.Any()).
		Return(db.Repository{}, errDefault)
func withRepoByID(mock dbMock) {
		Return(dbRepo, nil)
func withRepoByName(mock dbMock) {
		GetRepositoryByRepoName(gomock.Any(), gomock.Any()).
func withFailedDelete(mock dbMock) {
		DeleteRepository(gomock.Any(), gomock.Eq(repoID)).
		Return(errDefault)
func withSuccessfulDelete(mock dbMock) {
		DeleteRepository(gomock.Any(), gomock.Eq(repoID)).
		Return(nil)
Fixed code: 	ArtifactCmd.PersistentFlags().StringP("provider", "p", "", "Name of the provider, i.e. github")
	RepoCmd.PersistentFlags().StringP("provider", "p", "", "Name of the provider, i.e. github")
			Provider: sql.NullString{
				String: provider.Name,
				Valid:  true,
			},
SELECT * FROM repositories
    WHERE repo_owner = $1 AND repo_name = $2 AND project_id = $3
    AND lower(provider) = lower(sqlc.narg('provider')::text) OR sqlc.narg('provider')::text IS NULL;
WHERE project_id = $1
  AND lower(provider) = lower(COALESCE(sqlc.narg('provider'), provider)::text)
WHERE project_id = $1 AND webhook_id IS NOT NULL
    AND lower(provider) = lower(sqlc.narg('provider')::text) OR sqlc.narg('provider')::text IS NULL
	"github.com/stacklok/minder/internal/providers/github/oauth"
const defaultProvider = oauth.Github

	providerName string,
	name := getNameFilterParam(providerName)
	name := getNameFilterParam(in.GetContext().GetProvider())
func getNameFilterParam(providerName string) sql.NullString {
		String: providerName,
		Valid:  providerName != "",
	providerName := entityCtx.Provider.Name
	results, err := artifactFilter.listArtifacts(ctx, providerName, projectID)
	logger.BusinessRecord(ctx).Provider = providerName
	providerFilter := getNameFilterParam(entityCtx.Provider.Name)
		Provider:  providerFilter,
	providerFilter := getNameFilterParam(provider)

		db.ListRegisteredRepositoriesByProjectIDAndProviderParams{Provider: providerFilter, ProjectID: projectID})
	providerName := entityCtx.Provider.Name

	if providerName == "" {
		return nil, status.Errorf(codes.InvalidArgument, "provider name is required")
	}
	provider, err := s.store.GetProviderByName(ctx, db.GetProviderByNameParams{
		// We don't check parent projects here because subprojects should not update the credentials of their parent
		Projects: []uuid.UUID{projectID},
		Name:     providerName,
	})
	providerName := entityCtx.Provider.Name
	if providerName == "" {
		return nil, status.Errorf(codes.InvalidArgument, "provider name is required")
	}

	provider, err := s.store.GetProviderByName(ctx, db.GetProviderByNameParams{
		// We don't check parent projects here because subprojects should not check the credentials of their parent
		Projects: []uuid.UUID{projectID},
		Name:     providerName,
	})
	provider, err := getProviderFromRequestOrDefault(ctx, s.store, entityCtx.Provider.Name, entityCtx.Project.ID)
	provider, err := getProviderFromRequestOrDefault(ctx, s.store, entityCtx.Provider.Name, entityCtx.Project.ID)
	"fmt"
	"slices"
	providerName := getProviderName(ctx)
	provider, err := s.inferProviderByOwner(ctx, githubRepo.GetOwner(), projectID, providerName)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "cannot get provider: %v", err)
	}

	client, err := s.getClientForProvider(ctx, provider)
	if err != nil {
		return nil, err
	}

	newRepo, err := s.repos.CreateRepository(ctx, client, &provider, projectID, githubRepo.GetOwner(), githubRepo.GetName())
	providerName := entityCtx.Provider.Name
	providerFilter := getNameFilterParam(providerName)
	if reqRepoCursor.ProjectId == projectID.String() && reqRepoCursor.Provider == providerName {
		Provider:  providerFilter,
			Provider:  providerName,
	logger.BusinessRecord(ctx).Provider = providerName
	providerFilter := getNameFilterParam(entityCtx.Provider.Name)
		Provider:  providerFilter,
	projectID := getProjectID(ctx)

	err = s.deleteRepository(ctx, func() (db.Repository, error) {
		return s.repos.GetRepositoryById(ctx, parsedRepositoryID, projectID)
	projectID := getProjectID(ctx)
	providerName := getProviderName(ctx)

	err := s.deleteRepository(ctx, func() (db.Repository, error) {
		return s.repos.GetRepositoryByName(ctx, fragments[0], fragments[1], projectID, providerName)
// covers the common logic for the two varieties of repo deletion
func (s *Server) deleteRepository(
	repoQueryMethod func() (db.Repository, error),
) error {
	projectID := getProjectID(ctx)

	repo, err := repoQueryMethod()
	if errors.Is(err, sql.ErrNoRows) {
		return status.Errorf(codes.NotFound, "repository not found")
	} else if err != nil {
		return status.Errorf(codes.Internal, "unexpected error fetching repo: %v", err)
	}

	provider, err := s.findProviderByName(ctx, repo.Provider, projectID)
		return status.Errorf(codes.Internal, "cannot get provider: %v", err)
	client, err := s.getClientForProvider(ctx, provider)
	if err != nil {
		return status.Errorf(codes.Internal, "cannot get client for provider: %v", err)
	}

	err = s.repos.DeleteRepository(ctx, client, &repo)
	if err != nil {
		return status.Errorf(codes.Internal, "unexpected error deleting repo: %v", err)
	}
	return nil
}

// inferProviderByOwner returns the provider to use for a given repo owner
func (s *Server) inferProviderByOwner(ctx context.Context, owner string, projectID uuid.UUID, providerName string,
) (db.Provider, error) {
	if providerName != "" {
		return s.findProviderByName(ctx, providerName, projectID)
	}
	opts, err := getProvidersByTrait(ctx, s.store, nil, projectID, db.ProviderTypeGithub)
	if err != nil {
		return db.Provider{}, fmt.Errorf("error getting providers: %v", err)
	}

	slices.SortFunc(opts, func(a, b db.Provider) int {
		// Sort GitHub OAuth provider after all GitHub App providers
		if a.Class.ProviderClass == db.ProviderClassGithub && b.Class.ProviderClass == db.ProviderClassGithubApp {
			return 1
		}
		if a.Class.ProviderClass == db.ProviderClassGithubApp && b.Class.ProviderClass == db.ProviderClassGithub {
			return -1
		}
		return 0
	})

	for _, prov := range opts {
		if github.CanHandleOwner(ctx, prov, owner) {
			return prov, nil
		}
	}

	return db.Provider{}, fmt.Errorf("no providers can handle repo owned by %s", owner)
}

func (s *Server) getClientForProvider(
	ctx context.Context,
	provider db.Provider,
) (v1.GitHub, error) {
		return nil, status.Errorf(codes.Internal, "cannot get provider builder: %v", err)
		return nil, status.Errorf(codes.Internal, "error creating github provider: %v", err)
	return client, nil
func (s *Server) findProviderByName(ctx context.Context, providerName string, projectID uuid.UUID) (db.Provider, error) {
	ph, err := s.store.GetParentProjects(ctx, projectID)
		return db.Provider{}, fmt.Errorf("error getting project hierarchy: %v", err)
	provider, err := s.store.GetProviderByName(ctx, db.GetProviderByNameParams{
		Name:     providerName,
		Projects: ph,
	})
	if err != nil {
		return db.Provider{}, fmt.Errorf("error getting provider: %v", err)
	return provider, nil

func getProviderName(ctx context.Context) string {
	entityCtx := engine.EntityFromContext(ctx)
	return entityCtx.Provider.Name
}
			ExpectedError: "error getting provider",
			Name:             "deletion fails when provider cannot be found",
			RepoName:         repoOwnerAndName,
			RepoServiceSetup: newRepoService(withSuccessfulGetRepoByName),
			ProviderFails:    true,
			ExpectedError:    "error getting provider",
			RepoServiceSetup: newRepoService(withFailedGetRepoByName(sql.ErrNoRows)),
			RepoServiceSetup: newRepoService(withSuccessfulGetRepoByName, withFailedDelete(errDefault)),
			RepoServiceSetup: newRepoService(withSuccessfulGetRepoById, withFailedDelete(errDefault)),
			RepoServiceSetup: newRepoService(withSuccessfulGetRepoByName, withSuccessfulDelete),
			RepoServiceSetup: newRepoService(withSuccessfulGetRepoById, withSuccessfulDelete),
func withSuccessfulDelete(mock repoServiceMock) {
	withFailedDelete(nil)(mock)
func withFailedDelete(err error) func(repoServiceMock) {
			DeleteRepository(gomock.Any(), gomock.Any(), gomock.Any()).
func withSuccessfulGetRepoById(mock repoServiceMock) {
	mock.EXPECT().
		GetRepositoryById(gomock.Any(), gomock.Any(), gomock.Any()).
		Return(db.Repository{}, nil)
func withSuccessfulGetRepoByName(mock repoServiceMock) {
	mock.EXPECT().
		GetRepositoryByName(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
		Return(db.Repository{}, nil)
}

func withFailedGetRepoByName(err error) func(repoServiceMock) {
			GetRepositoryByName(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
			Return(db.Repository{}, err)
func createServer(ctrl *gomock.Controller, repoServiceSetup repoMockBuilder, providerFails bool) *Server {
			GetProviderByName(gomock.Any(), gomock.Any()).
			Return(db.Provider{}, errDefault)
			GetProviderByName(gomock.Any(), gomock.Any()).
			Return(provider, nil).AnyTimes()
SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories
    WHERE repo_owner = $1 AND repo_name = $2 AND project_id = $3
    AND lower(provider) = lower($4::text) OR $4::text IS NULL
	RepoOwner string         `json:"repo_owner"`
	RepoName  string         `json:"repo_name"`
	ProjectID uuid.UUID      `json:"project_id"`
	Provider  sql.NullString `json:"provider"`
		arg.Provider,
WHERE project_id = $1 AND webhook_id IS NOT NULL
    AND lower(provider) = lower($2::text) OR $2::text IS NULL
	ProjectID uuid.UUID      `json:"project_id"`
	Provider  sql.NullString `json:"provider"`
	rows, err := q.db.QueryContext(ctx, listRegisteredRepositoriesByProjectIDAndProvider, arg.ProjectID, arg.Provider)
WHERE project_id = $1
  AND (repo_id >= $2 OR $2 IS NULL)
  AND lower(provider) = lower(COALESCE($3, provider)::text)
	ProjectID uuid.UUID      `json:"project_id"`
	RepoID    sql.NullInt64  `json:"repo_id"`
	Provider  sql.NullString `json:"provider"`
	Limit     sql.NullInt64  `json:"limit"`
		arg.Provider,
		Provider: sql.NullString{
			String: repo1.Provider,
			Valid:  true,
		},
		Provider: sql.NullString{
			String: prov.Name,
			Valid:  true,
		},

// CanHandleOwner checks if the GitHub provider has the right credentials to handle the owner
func CanHandleOwner(_ context.Context, prov db.Provider, owner string) bool {
	// TODO: this is fragile and does not handle organization renames, in the future we can make sure the credential
	// has admin permissions on the owner
	if prov.Name == fmt.Sprintf("%s-%s", db.ProviderClassGithubApp, owner) {
		return true
	}
	if prov.Class.ProviderClass == db.ProviderClassGithub {
		return true
	}
	return false
}
			Provider: sql.NullString{
				String: ectx.Provider.Name,
				Valid:  ectx.Provider.Name != "",
			},
// DeleteRepository mocks base method.
func (m *MockRepositoryService) DeleteRepository(arg0 context.Context, arg1 clients.GitHubRepoClient, arg2 *db.Repository) error {
	ret := m.ctrl.Call(m, "DeleteRepository", arg0, arg1, arg2)
// DeleteRepository indicates an expected call of DeleteRepository.
func (mr *MockRepositoryServiceMockRecorder) DeleteRepository(arg0, arg1, arg2 any) *gomock.Call {
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRepository", reflect.TypeOf((*MockRepositoryService)(nil).DeleteRepository), arg0, arg1, arg2)
// GetRepositoryById mocks base method.
func (m *MockRepositoryService) GetRepositoryById(arg0 context.Context, arg1, arg2 uuid.UUID) (db.Repository, error) {
	ret := m.ctrl.Call(m, "GetRepositoryById", arg0, arg1, arg2)
	ret0, _ := ret[0].(db.Repository)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// GetRepositoryById indicates an expected call of GetRepositoryById.
func (mr *MockRepositoryServiceMockRecorder) GetRepositoryById(arg0, arg1, arg2 any) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryById", reflect.TypeOf((*MockRepositoryService)(nil).GetRepositoryById), arg0, arg1, arg2)
}

// GetRepositoryByName mocks base method.
func (m *MockRepositoryService) GetRepositoryByName(arg0 context.Context, arg1, arg2 string, arg3 uuid.UUID, arg4 string) (db.Repository, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "GetRepositoryByName", arg0, arg1, arg2, arg3, arg4)
	ret0, _ := ret[0].(db.Repository)
	ret1, _ := ret[1].(error)
	return ret0, ret1
// GetRepositoryByName indicates an expected call of GetRepositoryByName.
func (mr *MockRepositoryServiceMockRecorder) GetRepositoryByName(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call {
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryByName", reflect.TypeOf((*MockRepositoryService)(nil).GetRepositoryByName), arg0, arg1, arg2, arg3, arg4)
	// DeleteRepository removes the webhook and deletes the repo from the database.
	DeleteRepository(
		repo *db.Repository,
	// GetRepositoryById retrieves a repository by its ID and project.
	GetRepositoryById(ctx context.Context, repositoryID uuid.UUID, projectID uuid.UUID) (db.Repository, error)
	// GetRepositoryByName retrieves a repository by its name, owner, project and provider (if specified).
	GetRepositoryByName(
		repoOwner string,
		repoName string,
		providerName string,
	) (db.Repository, error)
func (r *repositoryService) GetRepositoryById(
	repositoryID uuid.UUID,
) (db.Repository, error) {
	return r.store.GetRepositoryByIDAndProject(ctx, db.GetRepositoryByIDAndProjectParams{
		ID:        repositoryID,
	})
func (r *repositoryService) GetRepositoryByName(
	repoOwner string,
	repoName string,
	providerName string,
) (db.Repository, error) {
	providerFilter := sql.NullString{
		String: providerName,
		Valid:  providerName != "",
	}
	params := db.GetRepositoryByRepoNameParams{
		Provider:  providerFilter,
		RepoOwner: repoOwner,
		RepoName:  repoName,
	return r.store.GetRepositoryByRepoName(ctx, params)
func (r *repositoryService) DeleteRepository(ctx context.Context, client ghclient.GitHubRepoClient, repo *db.Repository) error {
			Name:         "Delete fails when webhook cannot be deleted",
			DBSetup:      newDBMock(withFailedDelete),
			Name:          "Delete succeeds",
			DBSetup:       newDBMock(withSuccessfulDelete),
	}

	for i := range scenarios {
		scenario := scenarios[i]
		t.Run(scenario.Name, func(t *testing.T) {
			t.Parallel()
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			ctx := context.Background()

			// For the purposes of this test, we do not need to attach any
			// mock behaviour to the client. We can leave it as a nil pointer.
			var ghClient clients.GitHubRepoClient

			svc := createService(ctrl, scenario.WebhookSetup, scenario.DBSetup, false)
			err := svc.DeleteRepository(ctx, ghClient, &dbRepo)

			if scenario.ShouldSucceed {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
			}
		})
	}
}

func TestRepositoryService_GetRepositoryById(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		Name          string
		DBSetup       dbMockBuilder
		DeleteType    DeleteCallType
		ShouldSucceed bool
	}{
		{
			Name:    "Get by ID fails when DB call fails",
			DBSetup: newDBMock(withFailedGetById),
		},
			Name:          "Get by ID succeeds",
			DBSetup:       newDBMock(withSuccessfulGetById),
			svc := createService(ctrl, newWebhookMock(), scenario.DBSetup, false)
			_, err := svc.GetRepositoryById(ctx, repoID, projectID)

			if scenario.ShouldSucceed {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
		})
	}
}

func TestRepositoryService_GetRepositoryByName(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		Name          string
		DBSetup       dbMockBuilder
		DeleteType    DeleteCallType
		ShouldSucceed bool
	}{
		{
			Name:    "Get by name fails when DB call fails",
			DBSetup: newDBMock(withFailedGetByName),
		},
		{
			Name:          "Get by name succeeds",
			DBSetup:       newDBMock(withSuccessfulGetByName),
			ShouldSucceed: true,
		},
	}

	for i := range scenarios {
		scenario := scenarios[i]
		t.Run(scenario.Name, func(t *testing.T) {
			t.Parallel()
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()
			ctx := context.Background()

			// For the purposes of this test, we do not need to attach any
			// mock behaviour to the client. We can leave it as a nil pointer.

			svc := createService(ctrl, newWebhookMock(), scenario.DBSetup, false)
			_, err := svc.GetRepositoryByName(ctx, repoOwner, repoName, projectID, providerName)
func withFailedDelete(mock dbMock) {
		DeleteRepository(gomock.Any(), gomock.Eq(repoID)).
		Return(errDefault)
func withSuccessfulDelete(mock dbMock) {
		DeleteRepository(gomock.Any(), gomock.Eq(repoID)).
		Return(nil)
func withFailedGetById(mock dbMock) {
		Return(db.Repository{}, errDefault)
func withSuccessfulGetById(mock dbMock) {
		GetRepositoryByIDAndProject(gomock.Any(), gomock.Any()).
func withFailedGetByName(mock dbMock) {
		GetRepositoryByRepoName(gomock.Any(), gomock.Any()).
		Return(db.Repository{}, errDefault)
func withSuccessfulGetByName(mock dbMock) {
		GetRepositoryByRepoName(gomock.Any(), gomock.Any()).
		Return(dbRepo, nil)