github.com/argoproj/argo-cd/v3@v3.2.1/server/repository/repository.go (about)

     1  package repository
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"reflect"
     7  	"sort"
     8  
     9  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    10  	"github.com/argoproj/gitops-engine/pkg/utils/text"
    11  	log "github.com/sirupsen/logrus"
    12  	"google.golang.org/grpc/codes"
    13  	"google.golang.org/grpc/status"
    14  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/client-go/tools/cache"
    17  
    18  	"github.com/argoproj/argo-cd/v3/common"
    19  	repositorypkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/repository"
    20  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    21  	applisters "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
    22  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    23  	servercache "github.com/argoproj/argo-cd/v3/server/cache"
    24  	"github.com/argoproj/argo-cd/v3/util/argo"
    25  	"github.com/argoproj/argo-cd/v3/util/db"
    26  	"github.com/argoproj/argo-cd/v3/util/errors"
    27  	"github.com/argoproj/argo-cd/v3/util/git"
    28  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    29  	"github.com/argoproj/argo-cd/v3/util/rbac"
    30  	"github.com/argoproj/argo-cd/v3/util/settings"
    31  )
    32  
    33  // Server provides a Repository service
    34  type Server struct {
    35  	db              db.ArgoDB
    36  	repoClientset   apiclient.Clientset
    37  	enf             *rbac.Enforcer
    38  	cache           *servercache.Cache
    39  	appLister       applisters.ApplicationLister
    40  	projLister      cache.SharedIndexInformer
    41  	settings        *settings.SettingsManager
    42  	namespace       string
    43  	hydratorEnabled bool
    44  }
    45  
    46  // NewServer returns a new instance of the Repository service
    47  func NewServer(
    48  	repoClientset apiclient.Clientset,
    49  	db db.ArgoDB,
    50  	enf *rbac.Enforcer,
    51  	cache *servercache.Cache,
    52  	appLister applisters.ApplicationLister,
    53  	projLister cache.SharedIndexInformer,
    54  	namespace string,
    55  	settings *settings.SettingsManager,
    56  	hydratorEnabled bool,
    57  ) *Server {
    58  	return &Server{
    59  		db:              db,
    60  		repoClientset:   repoClientset,
    61  		enf:             enf,
    62  		cache:           cache,
    63  		appLister:       appLister,
    64  		projLister:      projLister,
    65  		namespace:       namespace,
    66  		settings:        settings,
    67  		hydratorEnabled: hydratorEnabled,
    68  	}
    69  }
    70  
    71  func (s *Server) getRepo(ctx context.Context, url, project string) (*v1alpha1.Repository, error) {
    72  	repo, err := s.db.GetRepository(ctx, url, project)
    73  	if err != nil {
    74  		return nil, common.PermissionDeniedAPIError
    75  	}
    76  	return repo, nil
    77  }
    78  
    79  func (s *Server) getWriteRepo(ctx context.Context, url, project string) (*v1alpha1.Repository, error) {
    80  	repo, err := s.db.GetWriteRepository(ctx, url, project)
    81  	if err != nil {
    82  		return nil, common.PermissionDeniedAPIError
    83  	}
    84  	return repo, nil
    85  }
    86  
    87  func createRBACObject(project string, repo string) string {
    88  	if project != "" {
    89  		return project + "/" + repo
    90  	}
    91  	return repo
    92  }
    93  
    94  // Get the connection state for a given repository URL by connecting to the
    95  // repo and evaluate the results. Unless forceRefresh is set to true, the
    96  // result may be retrieved out of the cache.
    97  func (s *Server) getConnectionState(ctx context.Context, url string, project string, forceRefresh bool) v1alpha1.ConnectionState {
    98  	if !forceRefresh {
    99  		if connectionState, err := s.cache.GetRepoConnectionState(url, project); err == nil {
   100  			return connectionState
   101  		}
   102  	}
   103  	now := metav1.Now()
   104  	connectionState := v1alpha1.ConnectionState{
   105  		Status:     v1alpha1.ConnectionStatusSuccessful,
   106  		ModifiedAt: &now,
   107  	}
   108  	var err error
   109  	repo, err := s.db.GetRepository(ctx, url, project)
   110  	if err == nil {
   111  		err = s.testRepo(ctx, repo)
   112  	}
   113  	if err != nil {
   114  		connectionState.Status = v1alpha1.ConnectionStatusFailed
   115  		if errors.IsCredentialsConfigurationError(err) {
   116  			connectionState.Message = "Configuration error - please check the server logs"
   117  			log.Warnf("could not retrieve repo: %s", err.Error())
   118  		} else {
   119  			connectionState.Message = fmt.Sprintf("Unable to connect to repository: %v", err)
   120  		}
   121  	}
   122  	err = s.cache.SetRepoConnectionState(url, project, &connectionState)
   123  	if err != nil {
   124  		log.Warnf("getConnectionState cache set error %s: %v", url, err)
   125  	}
   126  	return connectionState
   127  }
   128  
   129  // List returns list of repositories
   130  // Deprecated: Use ListRepositories instead
   131  func (s *Server) List(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.RepositoryList, error) {
   132  	return s.ListRepositories(ctx, q)
   133  }
   134  
   135  // Get return the requested configured repository by URL and the state of its connections.
   136  func (s *Server) Get(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.Repository, error) {
   137  	// ListRepositories normalizes the repo, sanitizes it, and augments it with connection details.
   138  	repo, err := getRepository(ctx, s.ListRepositories, q)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	exists, err := s.db.RepositoryExists(ctx, q.Repo, repo.Project)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	if !exists {
   152  		return nil, status.Errorf(codes.NotFound, "repo '%s' not found", q.Repo)
   153  	}
   154  
   155  	return repo, nil
   156  }
   157  
   158  func (s *Server) GetWrite(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.Repository, error) {
   159  	if !s.hydratorEnabled {
   160  		return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
   161  	}
   162  
   163  	repo, err := getRepository(ctx, s.ListWriteRepositories, q)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	exists, err := s.db.WriteRepositoryExists(ctx, q.Repo, repo.Project)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	if !exists {
   177  		return nil, status.Errorf(codes.NotFound, "write repo '%s' not found", q.Repo)
   178  	}
   179  
   180  	return repo, nil
   181  }
   182  
   183  // ListRepositories returns a list of all configured repositories and the state of their connections
   184  func (s *Server) ListRepositories(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.RepositoryList, error) {
   185  	repos, err := s.db.ListRepositories(ctx)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	items, err := s.prepareRepoList(ctx, rbac.ResourceRepositories, repos, q.ForceRefresh)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  	return &v1alpha1.RepositoryList{Items: items}, nil
   194  }
   195  
   196  // ListWriteRepositories returns a list of all configured repositories where the user has write access and the state of
   197  // their connections
   198  func (s *Server) ListWriteRepositories(ctx context.Context, q *repositorypkg.RepoQuery) (*v1alpha1.RepositoryList, error) {
   199  	if !s.hydratorEnabled {
   200  		return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
   201  	}
   202  
   203  	repos, err := s.db.ListWriteRepositories(ctx)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	items, err := s.prepareRepoList(ctx, rbac.ResourceWriteRepositories, repos, q.ForceRefresh)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	return &v1alpha1.RepositoryList{Items: items}, nil
   212  }
   213  
   214  // ListRepositoriesByAppProject returns a list of all configured repositories and the state of their connections. It
   215  // normalizes, sanitizes, and filters out repositories that the user does not have access to in the specified project.
   216  // It also sorts the repositories by project and repo name.
   217  func (s *Server) prepareRepoList(ctx context.Context, resourceType string, repos []*v1alpha1.Repository, forceRefresh bool) (v1alpha1.Repositories, error) {
   218  	items := v1alpha1.Repositories{}
   219  	for _, repo := range repos {
   220  		items = append(items, repo.Normalize().Sanitized())
   221  	}
   222  	items = items.Filter(func(r *v1alpha1.Repository) bool {
   223  		return s.enf.Enforce(ctx.Value("claims"), resourceType, rbac.ActionGet, createRBACObject(r.Project, r.Repo))
   224  	})
   225  	err := kube.RunAllAsync(len(items), func(i int) error {
   226  		items[i].ConnectionState = s.getConnectionState(ctx, items[i].Repo, items[i].Project, forceRefresh)
   227  		return nil
   228  	})
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  	sort.Slice(items, func(i, j int) bool {
   233  		first := items[i]
   234  		second := items[j]
   235  		return fmt.Sprintf("%s/%s", first.Project, first.Repo) < fmt.Sprintf("%s/%s", second.Project, second.Repo)
   236  	})
   237  	return items, nil
   238  }
   239  
   240  func (s *Server) ListOCITags(ctx context.Context, q *repositorypkg.RepoQuery) (*apiclient.Refs, error) {
   241  	repo, err := s.getRepo(ctx, q.Repo, q.GetAppProject())
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	conn, repoClient, err := s.repoClientset.NewRepoServerClient()
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  	defer utilio.Close(conn)
   255  
   256  	return repoClient.ListOCITags(ctx, &apiclient.ListRefsRequest{
   257  		Repo: repo,
   258  	})
   259  }
   260  
   261  func (s *Server) ListRefs(ctx context.Context, q *repositorypkg.RepoQuery) (*apiclient.Refs, error) {
   262  	repo, err := s.getRepo(ctx, q.Repo, q.GetAppProject())
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  
   267  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
   268  		return nil, err
   269  	}
   270  
   271  	conn, repoClient, err := s.repoClientset.NewRepoServerClient()
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  	defer utilio.Close(conn)
   276  
   277  	return repoClient.ListRefs(ctx, &apiclient.ListRefsRequest{
   278  		Repo: repo,
   279  	})
   280  }
   281  
   282  // ListApps performs discovery of a git repository for potential sources of applications. Used
   283  // as a convenience to the UI for auto-complete.
   284  func (s *Server) ListApps(ctx context.Context, q *repositorypkg.RepoAppsQuery) (*repositorypkg.RepoAppsResponse, error) {
   285  	repo, err := s.getRepo(ctx, q.Repo, q.GetAppProject())
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	claims := ctx.Value("claims")
   291  	if err := s.enf.EnforceErr(claims, rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
   292  		return nil, err
   293  	}
   294  
   295  	// This endpoint causes us to clone git repos & invoke config management tooling for the purposes
   296  	// of app discovery. Only allow this to happen if user has privileges to create or update the
   297  	// application which it wants to retrieve these details for.
   298  	appRBACresource := fmt.Sprintf("%s/%s", q.AppProject, q.AppName)
   299  	if !s.enf.Enforce(claims, rbac.ResourceApplications, rbac.ActionCreate, appRBACresource) &&
   300  		!s.enf.Enforce(claims, rbac.ResourceApplications, rbac.ActionUpdate, appRBACresource) {
   301  		return nil, common.PermissionDeniedAPIError
   302  	}
   303  	// Also ensure the repo is actually allowed in the project in question
   304  	if err := s.isRepoPermittedInProject(ctx, q.Repo, q.AppProject); err != nil {
   305  		return nil, err
   306  	}
   307  
   308  	// Test the repo
   309  	conn, repoClient, err := s.repoClientset.NewRepoServerClient()
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  	defer utilio.Close(conn)
   314  
   315  	apps, err := repoClient.ListApps(ctx, &apiclient.ListAppsRequest{
   316  		Repo:     repo,
   317  		Revision: q.Revision,
   318  	})
   319  	if err != nil {
   320  		return nil, err
   321  	}
   322  	items := make([]*repositorypkg.AppInfo, 0)
   323  	for app, appType := range apps.Apps {
   324  		items = append(items, &repositorypkg.AppInfo{Path: app, Type: appType})
   325  	}
   326  	return &repositorypkg.RepoAppsResponse{Items: items}, nil
   327  }
   328  
   329  // GetAppDetails shows parameter values to various config tools (e.g. helm/kustomize values)
   330  // This is used by UI for parameter form fields during app create & edit pages.
   331  // It is also used when showing history of parameters used in previous syncs in the app history.
   332  func (s *Server) GetAppDetails(ctx context.Context, q *repositorypkg.RepoAppDetailsQuery) (*apiclient.RepoAppDetailsResponse, error) {
   333  	if q.Source == nil {
   334  		return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
   335  	}
   336  	repo, err := s.getRepo(ctx, q.Source.RepoURL, q.GetAppProject())
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  	claims := ctx.Value("claims")
   341  	if err := s.enf.EnforceErr(claims, rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
   342  		return nil, err
   343  	}
   344  	appName, appNs := argo.ParseFromQualifiedName(q.AppName, s.settings.GetNamespace())
   345  	app, err := s.appLister.Applications(appNs).Get(appName)
   346  	appRBACObj := createRBACObject(q.AppProject, q.AppName)
   347  	// ensure caller has read privileges to app
   348  	if err := s.enf.EnforceErr(claims, rbac.ResourceApplications, rbac.ActionGet, appRBACObj); err != nil {
   349  		return nil, err
   350  	}
   351  	if apierrors.IsNotFound(err) {
   352  		// app doesn't exist since it still is being formulated. verify they can create the app
   353  		// before we reveal repo details
   354  		if err := s.enf.EnforceErr(claims, rbac.ResourceApplications, rbac.ActionCreate, appRBACObj); err != nil {
   355  			return nil, err
   356  		}
   357  	} else {
   358  		// if we get here we are returning repo details of an existing app
   359  		if q.AppProject != app.Spec.Project {
   360  			return nil, common.PermissionDeniedAPIError
   361  		}
   362  		// verify caller is not making a request with arbitrary source values which were not in our history
   363  		if !isSourceInHistory(app, *q.Source, q.SourceIndex, q.VersionId) {
   364  			return nil, common.PermissionDeniedAPIError
   365  		}
   366  	}
   367  	// Ensure the repo is actually allowed in the project in question
   368  	if err := s.isRepoPermittedInProject(ctx, q.Source.RepoURL, q.AppProject); err != nil {
   369  		return nil, err
   370  	}
   371  
   372  	conn, repoClient, err := s.repoClientset.NewRepoServerClient()
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	defer utilio.Close(conn)
   377  	helmRepos, err := s.db.ListHelmRepositories(ctx)
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	kustomizeSettings, err := s.settings.GetKustomizeSettings()
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  	helmOptions, err := s.settings.GetHelmSettings()
   386  	if err != nil {
   387  		return nil, err
   388  	}
   389  
   390  	refSources := make(v1alpha1.RefTargetRevisionMapping)
   391  	if app != nil && app.Spec.HasMultipleSources() {
   392  		// Store the map of all sources having ref field into a map for applications with sources field
   393  		refSources, err = argo.GetRefSources(ctx, app.Spec.Sources, q.AppProject, s.db.GetRepository, []string{})
   394  		if err != nil {
   395  			return nil, fmt.Errorf("failed to get ref sources: %w", err)
   396  		}
   397  	}
   398  
   399  	return repoClient.GetAppDetails(ctx, &apiclient.RepoServerAppDetailsQuery{
   400  		Repo:             repo,
   401  		Source:           q.Source,
   402  		Repos:            helmRepos,
   403  		KustomizeOptions: kustomizeSettings,
   404  		HelmOptions:      helmOptions,
   405  		AppName:          q.AppName,
   406  		RefSources:       refSources,
   407  	})
   408  }
   409  
   410  // GetHelmCharts returns list of helm charts in the specified repository
   411  func (s *Server) GetHelmCharts(ctx context.Context, q *repositorypkg.RepoQuery) (*apiclient.HelmChartsResponse, error) {
   412  	repo, err := s.getRepo(ctx, q.Repo, q.GetAppProject())
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionGet, createRBACObject(repo.Project, repo.Repo)); err != nil {
   417  		return nil, err
   418  	}
   419  	conn, repoClient, err := s.repoClientset.NewRepoServerClient()
   420  	if err != nil {
   421  		return nil, err
   422  	}
   423  	defer utilio.Close(conn)
   424  	return repoClient.GetHelmCharts(ctx, &apiclient.HelmChartsRequest{Repo: repo})
   425  }
   426  
   427  // Create creates a repository or repository credential set
   428  // Deprecated: Use CreateRepository() instead
   429  func (s *Server) Create(ctx context.Context, q *repositorypkg.RepoCreateRequest) (*v1alpha1.Repository, error) {
   430  	return s.CreateRepository(ctx, q)
   431  }
   432  
   433  // CreateRepository creates a repository configuration
   434  func (s *Server) CreateRepository(ctx context.Context, q *repositorypkg.RepoCreateRequest) (*v1alpha1.Repository, error) {
   435  	if q.Repo == nil {
   436  		return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
   437  	}
   438  
   439  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionCreate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	var repo *v1alpha1.Repository
   444  	var err error
   445  
   446  	// check we can connect to the repo, copying any existing creds (not supported for project scoped repositories)
   447  	if q.Repo.Project == "" {
   448  		repo := q.Repo.DeepCopy()
   449  		if !repo.HasCredentials() {
   450  			creds, err := s.db.GetRepositoryCredentials(ctx, repo.Repo)
   451  			if err != nil {
   452  				return nil, err
   453  			}
   454  			repo.CopyCredentialsFrom(creds)
   455  		}
   456  
   457  		err = s.testRepo(ctx, repo)
   458  		if err != nil {
   459  			return nil, err
   460  		}
   461  	}
   462  
   463  	r := q.Repo
   464  	r.ConnectionState = v1alpha1.ConnectionState{Status: v1alpha1.ConnectionStatusSuccessful}
   465  	repo, err = s.db.CreateRepository(ctx, r)
   466  	if status.Convert(err).Code() == codes.AlreadyExists {
   467  		// act idempotent if existing spec matches new spec
   468  		existing, getErr := s.db.GetRepository(ctx, r.Repo, q.Repo.Project)
   469  		if getErr != nil {
   470  			return nil, status.Errorf(codes.Internal, "unable to check existing repository details: %v", getErr)
   471  		}
   472  
   473  		existing.Type = text.FirstNonEmpty(existing.Type, "git")
   474  		// repository ConnectionState may differ, so make consistent before testing
   475  		existing.ConnectionState = r.ConnectionState
   476  		switch {
   477  		case reflect.DeepEqual(existing, r):
   478  			repo, err = existing, nil
   479  		case q.Upsert:
   480  			r.Project = q.Repo.Project
   481  			return s.db.UpdateRepository(ctx, r)
   482  		default:
   483  			return nil, status.Error(codes.InvalidArgument, argo.GenerateSpecIsDifferentErrorMessage("repository", existing, r))
   484  		}
   485  	}
   486  	if err != nil {
   487  		return nil, err
   488  	}
   489  	return &v1alpha1.Repository{Repo: repo.Repo, Type: repo.Type, Name: repo.Name}, nil
   490  }
   491  
   492  // CreateWriteRepository creates a repository configuration with write credentials
   493  func (s *Server) CreateWriteRepository(ctx context.Context, q *repositorypkg.RepoCreateRequest) (*v1alpha1.Repository, error) {
   494  	if !s.hydratorEnabled {
   495  		return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
   496  	}
   497  
   498  	if q.Repo == nil {
   499  		return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
   500  	}
   501  
   502  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionCreate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil {
   503  		return nil, err
   504  	}
   505  
   506  	if !q.Repo.HasCredentials() {
   507  		return nil, status.Errorf(codes.InvalidArgument, "missing credentials in request")
   508  	}
   509  
   510  	err := s.testRepo(ctx, q.Repo)
   511  	if err != nil {
   512  		return nil, err
   513  	}
   514  
   515  	repo, err := s.db.CreateWriteRepository(ctx, q.Repo)
   516  	if status.Convert(err).Code() == codes.AlreadyExists {
   517  		// act idempotent if existing spec matches new spec
   518  		existing, getErr := s.db.GetWriteRepository(ctx, q.Repo.Repo, q.Repo.Project)
   519  		if getErr != nil {
   520  			return nil, status.Errorf(codes.Internal, "unable to check existing repository details: %v", getErr)
   521  		}
   522  		switch {
   523  		case reflect.DeepEqual(existing, q.Repo):
   524  			repo, err = existing, nil
   525  		case q.Upsert:
   526  			return s.db.UpdateWriteRepository(ctx, q.Repo)
   527  		default:
   528  			return nil, status.Error(codes.InvalidArgument, argo.GenerateSpecIsDifferentErrorMessage("write repository", existing, q.Repo))
   529  		}
   530  	}
   531  	if err != nil {
   532  		return nil, err
   533  	}
   534  	return &v1alpha1.Repository{Repo: repo.Repo, Type: repo.Type, Name: repo.Name}, nil
   535  }
   536  
   537  // Update updates a repository or credential set
   538  // Deprecated: Use UpdateRepository() instead
   539  func (s *Server) Update(ctx context.Context, q *repositorypkg.RepoUpdateRequest) (*v1alpha1.Repository, error) {
   540  	return s.UpdateRepository(ctx, q)
   541  }
   542  
   543  // UpdateRepository updates a repository configuration
   544  func (s *Server) UpdateRepository(ctx context.Context, q *repositorypkg.RepoUpdateRequest) (*v1alpha1.Repository, error) {
   545  	if q.Repo == nil {
   546  		return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
   547  	}
   548  
   549  	repo, err := s.getRepo(ctx, q.Repo.Repo, q.Repo.Project)
   550  	if err != nil {
   551  		return nil, err
   552  	}
   553  
   554  	// verify that user can do update inside project where repository is located
   555  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionUpdate, createRBACObject(repo.Project, repo.Repo)); err != nil {
   556  		return nil, err
   557  	}
   558  	// verify that user can do update inside project where repository will be located
   559  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionUpdate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil {
   560  		return nil, err
   561  	}
   562  	_, err = s.db.UpdateRepository(ctx, q.Repo)
   563  	return &v1alpha1.Repository{Repo: q.Repo.Repo, Type: q.Repo.Type, Name: q.Repo.Name}, err
   564  }
   565  
   566  // UpdateWriteRepository updates a repository configuration with write credentials
   567  func (s *Server) UpdateWriteRepository(ctx context.Context, q *repositorypkg.RepoUpdateRequest) (*v1alpha1.Repository, error) {
   568  	if !s.hydratorEnabled {
   569  		return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
   570  	}
   571  
   572  	if q.Repo == nil {
   573  		return nil, status.Errorf(codes.InvalidArgument, "missing payload in request")
   574  	}
   575  
   576  	repo, err := s.getWriteRepo(ctx, q.Repo.Repo, q.Repo.Project)
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  
   581  	// verify that user can do update inside project where repository is located
   582  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionUpdate, createRBACObject(repo.Project, repo.Repo)); err != nil {
   583  		return nil, err
   584  	}
   585  	// verify that user can do update inside project where repository will be located
   586  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionUpdate, createRBACObject(q.Repo.Project, q.Repo.Repo)); err != nil {
   587  		return nil, err
   588  	}
   589  	_, err = s.db.UpdateWriteRepository(ctx, q.Repo)
   590  	return &v1alpha1.Repository{Repo: q.Repo.Repo, Type: q.Repo.Type, Name: q.Repo.Name}, err
   591  }
   592  
   593  // Delete removes a repository from the configuration
   594  // Deprecated: Use DeleteRepository() instead
   595  func (s *Server) Delete(ctx context.Context, q *repositorypkg.RepoQuery) (*repositorypkg.RepoResponse, error) {
   596  	return s.DeleteRepository(ctx, q)
   597  }
   598  
   599  // DeleteRepository removes a repository from the configuration
   600  func (s *Server) DeleteRepository(ctx context.Context, q *repositorypkg.RepoQuery) (*repositorypkg.RepoResponse, error) {
   601  	repo, err := getRepository(ctx, s.ListRepositories, q)
   602  	if err != nil {
   603  		return nil, err
   604  	}
   605  
   606  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionDelete, createRBACObject(repo.Project, repo.Repo)); err != nil {
   607  		return nil, err
   608  	}
   609  
   610  	// invalidate cache
   611  	if err := s.cache.SetRepoConnectionState(repo.Repo, repo.Project, nil); err != nil {
   612  		log.Errorf("error invalidating cache: %v", err)
   613  	}
   614  
   615  	err = s.db.DeleteRepository(ctx, repo.Repo, repo.Project)
   616  	return &repositorypkg.RepoResponse{}, err
   617  }
   618  
   619  // DeleteWriteRepository removes a repository from the configuration
   620  func (s *Server) DeleteWriteRepository(ctx context.Context, q *repositorypkg.RepoQuery) (*repositorypkg.RepoResponse, error) {
   621  	if !s.hydratorEnabled {
   622  		return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
   623  	}
   624  
   625  	repo, err := getRepository(ctx, s.ListWriteRepositories, q)
   626  	if err != nil {
   627  		return nil, err
   628  	}
   629  
   630  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionDelete, createRBACObject(repo.Project, repo.Repo)); err != nil {
   631  		return nil, err
   632  	}
   633  
   634  	err = s.db.DeleteWriteRepository(ctx, repo.Repo, repo.Project)
   635  	return &repositorypkg.RepoResponse{}, err
   636  }
   637  
   638  // getRepository fetches a single repository which the user has access to. If only one repository can be found which
   639  // matches the same URL, that will be returned (this is for backward compatibility reasons). If multiple repositories
   640  // are matched, a repository is only returned if it matches the app project of the incoming request.
   641  func getRepository(ctx context.Context, listRepositories func(context.Context, *repositorypkg.RepoQuery) (*v1alpha1.RepositoryList, error), q *repositorypkg.RepoQuery) (*v1alpha1.Repository, error) {
   642  	repositories, err := listRepositories(ctx, q)
   643  	if err != nil {
   644  		return nil, err
   645  	}
   646  
   647  	var foundRepos []*v1alpha1.Repository
   648  	for _, repo := range repositories.Items {
   649  		if git.SameURL(repo.Repo, q.Repo) {
   650  			foundRepos = append(foundRepos, repo)
   651  		}
   652  	}
   653  
   654  	if len(foundRepos) == 0 {
   655  		return nil, common.PermissionDeniedAPIError
   656  	}
   657  
   658  	var foundRepo *v1alpha1.Repository
   659  	if len(foundRepos) == 1 && q.GetAppProject() == "" {
   660  		foundRepo = foundRepos[0]
   661  	} else if len(foundRepos) > 0 {
   662  		for _, repo := range foundRepos {
   663  			if repo.Project == q.GetAppProject() {
   664  				foundRepo = repo
   665  				break
   666  			}
   667  		}
   668  	}
   669  
   670  	if foundRepo == nil {
   671  		return nil, fmt.Errorf("repository not found for url %q and project %q", q.Repo, q.GetAppProject())
   672  	}
   673  
   674  	return foundRepo, nil
   675  }
   676  
   677  // ValidateAccess checks whether access to a repository is possible with the
   678  // given URL and credentials.
   679  func (s *Server) ValidateAccess(ctx context.Context, q *repositorypkg.RepoAccessQuery) (*repositorypkg.RepoResponse, error) {
   680  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceRepositories, rbac.ActionCreate, createRBACObject(q.Project, q.Repo)); err != nil {
   681  		return nil, err
   682  	}
   683  
   684  	repo := &v1alpha1.Repository{
   685  		Repo:                       q.Repo,
   686  		Type:                       q.Type,
   687  		Name:                       q.Name,
   688  		Username:                   q.Username,
   689  		Password:                   q.Password,
   690  		BearerToken:                q.BearerToken,
   691  		SSHPrivateKey:              q.SshPrivateKey,
   692  		Insecure:                   q.Insecure,
   693  		TLSClientCertData:          q.TlsClientCertData,
   694  		TLSClientCertKey:           q.TlsClientCertKey,
   695  		EnableOCI:                  q.EnableOci,
   696  		GithubAppPrivateKey:        q.GithubAppPrivateKey,
   697  		GithubAppId:                q.GithubAppID,
   698  		GithubAppInstallationId:    q.GithubAppInstallationID,
   699  		GitHubAppEnterpriseBaseURL: q.GithubAppEnterpriseBaseUrl,
   700  		Proxy:                      q.Proxy,
   701  		GCPServiceAccountKey:       q.GcpServiceAccountKey,
   702  		InsecureOCIForceHttp:       q.InsecureOciForceHttp,
   703  		UseAzureWorkloadIdentity:   q.UseAzureWorkloadIdentity,
   704  	}
   705  
   706  	// If repo does not have credentials, check if there are credentials stored
   707  	// for it and if yes, copy them
   708  	if !repo.HasCredentials() {
   709  		repoCreds, err := s.db.GetRepositoryCredentials(ctx, q.Repo)
   710  		if err != nil {
   711  			return nil, err
   712  		}
   713  		if repoCreds != nil {
   714  			repo.CopyCredentialsFrom(repoCreds)
   715  		}
   716  	}
   717  	err := s.testRepo(ctx, repo)
   718  	if err != nil {
   719  		return nil, err
   720  	}
   721  	return &repositorypkg.RepoResponse{}, nil
   722  }
   723  
   724  // ValidateWriteAccess checks whether write access to a repository is possible with the
   725  // given URL and credentials.
   726  func (s *Server) ValidateWriteAccess(ctx context.Context, q *repositorypkg.RepoAccessQuery) (*repositorypkg.RepoResponse, error) {
   727  	if !s.hydratorEnabled {
   728  		return nil, status.Error(codes.Unimplemented, "hydrator is disabled")
   729  	}
   730  
   731  	if err := s.enf.EnforceErr(ctx.Value("claims"), rbac.ResourceWriteRepositories, rbac.ActionCreate, createRBACObject(q.Project, q.Repo)); err != nil {
   732  		return nil, err
   733  	}
   734  
   735  	repo := &v1alpha1.Repository{
   736  		Repo:                       q.Repo,
   737  		Type:                       q.Type,
   738  		Name:                       q.Name,
   739  		Username:                   q.Username,
   740  		Password:                   q.Password,
   741  		BearerToken:                q.BearerToken,
   742  		SSHPrivateKey:              q.SshPrivateKey,
   743  		Insecure:                   q.Insecure,
   744  		TLSClientCertData:          q.TlsClientCertData,
   745  		TLSClientCertKey:           q.TlsClientCertKey,
   746  		EnableOCI:                  q.EnableOci,
   747  		GithubAppPrivateKey:        q.GithubAppPrivateKey,
   748  		GithubAppId:                q.GithubAppID,
   749  		GithubAppInstallationId:    q.GithubAppInstallationID,
   750  		GitHubAppEnterpriseBaseURL: q.GithubAppEnterpriseBaseUrl,
   751  		Proxy:                      q.Proxy,
   752  		GCPServiceAccountKey:       q.GcpServiceAccountKey,
   753  		UseAzureWorkloadIdentity:   q.UseAzureWorkloadIdentity,
   754  	}
   755  
   756  	err := s.testRepo(ctx, repo)
   757  	if err != nil {
   758  		return nil, err
   759  	}
   760  	return &repositorypkg.RepoResponse{}, nil
   761  }
   762  
   763  func (s *Server) testRepo(ctx context.Context, repo *v1alpha1.Repository) error {
   764  	conn, repoClient, err := s.repoClientset.NewRepoServerClient()
   765  	if err != nil {
   766  		return fmt.Errorf("failed to connect to repo-server: %w", err)
   767  	}
   768  	defer utilio.Close(conn)
   769  
   770  	_, err = repoClient.TestRepository(ctx, &apiclient.TestRepositoryRequest{
   771  		Repo: repo,
   772  	})
   773  	return err
   774  }
   775  
   776  func (s *Server) isRepoPermittedInProject(ctx context.Context, repo string, projName string) error {
   777  	proj, err := argo.GetAppProjectByName(ctx, projName, applisters.NewAppProjectLister(s.projLister.GetIndexer()), s.namespace, s.settings, s.db)
   778  	if err != nil {
   779  		return err
   780  	}
   781  	if !proj.IsSourcePermitted(v1alpha1.ApplicationSource{RepoURL: repo}) {
   782  		return status.Errorf(codes.PermissionDenied, "repository '%s' not permitted in project '%s'", repo, projName)
   783  	}
   784  	return nil
   785  }
   786  
   787  // isSourceInHistory checks if the supplied application source is either our current application
   788  // source, or was something which we synced to previously.
   789  func isSourceInHistory(app *v1alpha1.Application, source v1alpha1.ApplicationSource, index int32, versionId int32) bool {
   790  	// We have to check if the spec is within the source or sources split
   791  	// and then iterate over the historical
   792  	if app.Spec.HasMultipleSources() {
   793  		appSources := app.Spec.GetSources()
   794  		for _, s := range appSources {
   795  			if source.Equals(&s) {
   796  				return true
   797  			}
   798  		}
   799  	} else {
   800  		appSource := app.Spec.GetSource()
   801  		if source.Equals(&appSource) {
   802  			return true
   803  		}
   804  	}
   805  
   806  	// Iterate history. When comparing items in our history, use the actual synced revision to
   807  	// compare with the supplied source.targetRevision in the request. This is because
   808  	// history[].source.targetRevision is ambiguous (e.g. HEAD), whereas
   809  	// history[].revision will contain the explicit SHA
   810  	// In case of multi source apps, we have to check the specific versionID because users
   811  	// could have removed/added new sources and we cannot check all the versions due to that
   812  	for _, h := range app.Status.History {
   813  		// multi source revision
   814  		if len(h.Sources) > 0 {
   815  			if h.ID == int64(versionId) {
   816  				if h.Revisions == nil {
   817  					continue
   818  				}
   819  				h.Sources[index].TargetRevision = h.Revisions[index]
   820  				if source.Equals(&h.Sources[index]) {
   821  					return true
   822  				}
   823  			}
   824  		} else { // single source revision
   825  			h.Source.TargetRevision = h.Revision
   826  			if source.Equals(&h.Source) {
   827  				return true
   828  			}
   829  		}
   830  	}
   831  
   832  	return false
   833  }