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 }