CVE-2024-31455 (Go, published 2024-04-09T17:16Z)
Description: 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`.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/stacklok/minder
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 message: Make provider an optional filter (#2871)

Ref #2522

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)