github.com/argoproj/argo-cd/v2@v2.10.9/server/extension/extension.go (about) 1 package extension 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "net/http" 9 "net/http/httputil" 10 "net/url" 11 "regexp" 12 "strings" 13 "time" 14 15 log "github.com/sirupsen/logrus" 16 "gopkg.in/yaml.v3" 17 18 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 19 applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1" 20 "github.com/argoproj/argo-cd/v2/server/rbacpolicy" 21 "github.com/argoproj/argo-cd/v2/util/argo" 22 "github.com/argoproj/argo-cd/v2/util/db" 23 "github.com/argoproj/argo-cd/v2/util/security" 24 "github.com/argoproj/argo-cd/v2/util/settings" 25 ) 26 27 const ( 28 URLPrefix = "/extensions" 29 DefaultConnectionTimeout = 2 * time.Second 30 DefaultKeepAlive = 15 * time.Second 31 DefaultIdleConnectionTimeout = 60 * time.Second 32 DefaultMaxIdleConnections = 30 33 34 // HeaderArgoCDApplicationName defines the name of the 35 // expected application header to be passed to the extension 36 // handler. The header value must follow the format: 37 // "<namespace>:<app-name>" 38 // Example: 39 // Argocd-Application-Name: "namespace:app-name" 40 HeaderArgoCDApplicationName = "Argocd-Application-Name" 41 42 // HeaderArgoCDProjectName defines the name of the expected 43 // project header to be passed to the extension handler. 44 // Example: 45 // Argocd-Project-Name: "default" 46 HeaderArgoCDProjectName = "Argocd-Project-Name" 47 48 // HeaderArgoCDTargetClusterURL defines the target cluster URL 49 // that the Argo CD application is associated with. This header 50 // will be populated by the extension proxy and passed to the 51 // configured backend service. If this header is passed by 52 // the client, its value will be overriden by the extension 53 // handler. 54 // 55 // Example: 56 // Argocd-Target-Cluster-URL: "https://kubernetes.default.svc.cluster.local" 57 HeaderArgoCDTargetClusterURL = "Argocd-Target-Cluster-URL" 58 59 // HeaderArgoCDTargetClusterName defines the target cluster name 60 // that the Argo CD application is associated with. This header 61 // will be populated by the extension proxy and passed to the 62 // configured backend service. If this header is passed by 63 // the client, its value will be overriden by the extension 64 // handler. 65 HeaderArgoCDTargetClusterName = "Argocd-Target-Cluster-Name" 66 ) 67 68 // RequestResources defines the authorization scope for 69 // an incoming request to a given extension. This struct 70 // is populated from pre-defined Argo CD headers. 71 type RequestResources struct { 72 ApplicationName string 73 ApplicationNamespace string 74 ProjectName string 75 } 76 77 // ValidateHeaders will validate the pre-defined Argo CD 78 // request headers for extensions and extract the resources 79 // information populating and returning a RequestResources 80 // object. 81 // The pre-defined headers are: 82 // - Argocd-Application-Name 83 // - Argocd-Project-Name 84 // 85 // The headers expected format is documented in each of the constant 86 // types defined for them. 87 func ValidateHeaders(r *http.Request) (*RequestResources, error) { 88 appHeader := r.Header.Get(HeaderArgoCDApplicationName) 89 if appHeader == "" { 90 return nil, fmt.Errorf("header %q must be provided", HeaderArgoCDApplicationName) 91 } 92 appNamespace, appName, err := getAppName(appHeader) 93 if err != nil { 94 return nil, fmt.Errorf("error getting app details: %s", err) 95 } 96 if !argo.IsValidNamespaceName(appNamespace) { 97 return nil, errors.New("invalid value for namespace") 98 } 99 if !argo.IsValidAppName(appName) { 100 return nil, errors.New("invalid value for application name") 101 } 102 103 projName := r.Header.Get(HeaderArgoCDProjectName) 104 if projName == "" { 105 return nil, fmt.Errorf("header %q must be provided", HeaderArgoCDProjectName) 106 } 107 if !argo.IsValidProjectName(projName) { 108 return nil, errors.New("invalid value for project name") 109 } 110 return &RequestResources{ 111 ApplicationName: appName, 112 ApplicationNamespace: appNamespace, 113 ProjectName: projName, 114 }, nil 115 } 116 117 func getAppName(appHeader string) (string, string, error) { 118 parts := strings.Split(appHeader, ":") 119 if len(parts) != 2 { 120 return "", "", fmt.Errorf("invalid value for %q header: expected format: <namespace>:<app-name>", HeaderArgoCDApplicationName) 121 } 122 return parts[0], parts[1], nil 123 } 124 125 // ExtensionConfigs defines the configurations for all extensions 126 // retrieved from Argo CD configmap (argocd-cm). 127 type ExtensionConfigs struct { 128 Extensions []ExtensionConfig `yaml:"extensions"` 129 } 130 131 // ExtensionConfig defines the configuration for one extension. 132 type ExtensionConfig struct { 133 // Name defines the endpoint that will be used to register 134 // the extension route. Mandatory field. 135 Name string `yaml:"name"` 136 Backend BackendConfig `yaml:"backend"` 137 } 138 139 // BackendConfig defines the backend service configurations that will 140 // be used by an specific extension. An extension can have multiple services 141 // associated. This is necessary when Argo CD is managing applications in 142 // external clusters. In this case, each cluster may have its own backend 143 // service. 144 type BackendConfig struct { 145 ProxyConfig 146 Services []ServiceConfig `yaml:"services"` 147 } 148 149 // ServiceConfig provides the configuration for a backend service. 150 type ServiceConfig struct { 151 // URL is the address where the extension backend must be available. 152 // Mandatory field. 153 URL string `yaml:"url"` 154 155 // Cluster if provided, will have to match the application 156 // destination name to have requests properly forwarded to this 157 // service URL. 158 Cluster *ClusterConfig `yaml:"cluster,omitempty"` 159 160 // Headers if provided, the headers list will be added on all 161 // outgoing requests for this service config. 162 Headers []Header `yaml:"headers"` 163 } 164 165 // Header defines the header to be added in the proxy requests. 166 type Header struct { 167 // Name defines the name of the header. It is a mandatory field if 168 // a header is provided. 169 Name string `yaml:"name"` 170 // Value defines the value of the header. The actual value can be 171 // provided as verbatim or as a reference to an Argo CD secret key. 172 // In order to provide it as a reference, it is necessary to prefix 173 // it with a dollar sign. 174 // Example: 175 // value: '$some.argocd.secret.key' 176 // In the example above, the value will be replaced with the one from 177 // the argocd-secret with key 'some.argocd.secret.key'. 178 Value string `yaml:"value"` 179 } 180 181 type ClusterConfig struct { 182 // Server specifies the URL of the target cluster's Kubernetes control plane API. This must be set if Name is not set. 183 Server string `yaml:"server"` 184 185 // Name is an alternate way of specifying the target cluster by its symbolic name. This must be set if Server is not set. 186 Name string `yaml:"name"` 187 } 188 189 // ProxyConfig allows configuring connection behaviour between Argo CD 190 // API Server and the backend service. 191 type ProxyConfig struct { 192 // ConnectionTimeout is the maximum amount of time a dial to 193 // the extension server will wait for a connect to complete. 194 // Default: 2 seconds 195 ConnectionTimeout time.Duration `yaml:"connectionTimeout"` 196 197 // KeepAlive specifies the interval between keep-alive probes 198 // for an active network connection between the API server and 199 // the extension server. 200 // Default: 15 seconds 201 KeepAlive time.Duration `yaml:"keepAlive"` 202 203 // IdleConnectionTimeout is the maximum amount of time an idle 204 // (keep-alive) connection between the API server and the extension 205 // server will remain idle before closing itself. 206 // Default: 60 seconds 207 IdleConnectionTimeout time.Duration `yaml:"idleConnectionTimeout"` 208 209 // MaxIdleConnections controls the maximum number of idle (keep-alive) 210 // connections between the API server and the extension server. 211 // Default: 30 212 MaxIdleConnections int `yaml:"maxIdleConnections"` 213 } 214 215 // SettingsGetter defines the contract to retrieve Argo CD Settings. 216 type SettingsGetter interface { 217 Get() (*settings.ArgoCDSettings, error) 218 } 219 220 // DefaultSettingsGetter is the real settings getter implementation. 221 type DefaultSettingsGetter struct { 222 settingsMgr *settings.SettingsManager 223 } 224 225 // NewDefaultSettingsGetter returns a new default settings getter. 226 func NewDefaultSettingsGetter(mgr *settings.SettingsManager) *DefaultSettingsGetter { 227 return &DefaultSettingsGetter{ 228 settingsMgr: mgr, 229 } 230 } 231 232 // Get will retrieve the Argo CD settings. 233 func (s *DefaultSettingsGetter) Get() (*settings.ArgoCDSettings, error) { 234 return s.settingsMgr.GetSettings() 235 } 236 237 // ProjectGetter defines the contract to retrieve Argo CD Project. 238 type ProjectGetter interface { 239 Get(name string) (*v1alpha1.AppProject, error) 240 GetClusters(project string) ([]*v1alpha1.Cluster, error) 241 } 242 243 // DefaultProjectGetter is the real ProjectGetter implementation. 244 type DefaultProjectGetter struct { 245 projLister applisters.AppProjectNamespaceLister 246 db db.ArgoDB 247 } 248 249 // NewDefaultProjectGetter returns a new default project getter 250 func NewDefaultProjectGetter(lister applisters.AppProjectNamespaceLister, db db.ArgoDB) *DefaultProjectGetter { 251 return &DefaultProjectGetter{ 252 projLister: lister, 253 db: db, 254 } 255 } 256 257 // Get will retrieve the live AppProject state. 258 func (p *DefaultProjectGetter) Get(name string) (*v1alpha1.AppProject, error) { 259 return p.projLister.Get(name) 260 } 261 262 // GetClusters will retrieve the clusters configured by a project. 263 func (p *DefaultProjectGetter) GetClusters(project string) ([]*v1alpha1.Cluster, error) { 264 return p.db.GetProjectClusters(context.TODO(), project) 265 } 266 267 // ApplicationGetter defines the contract to retrieve the application resource. 268 type ApplicationGetter interface { 269 Get(ns, name string) (*v1alpha1.Application, error) 270 } 271 272 // DefaultApplicationGetter is the real application getter implementation. 273 type DefaultApplicationGetter struct { 274 appLister applisters.ApplicationLister 275 } 276 277 // NewDefaultApplicationGetter returns the default application getter. 278 func NewDefaultApplicationGetter(al applisters.ApplicationLister) *DefaultApplicationGetter { 279 return &DefaultApplicationGetter{ 280 appLister: al, 281 } 282 } 283 284 // Get will retrieve the application resorce for the given namespace and name. 285 func (a *DefaultApplicationGetter) Get(ns, name string) (*v1alpha1.Application, error) { 286 return a.appLister.Applications(ns).Get(name) 287 } 288 289 // RbacEnforcer defines the contract to enforce rbac rules 290 type RbacEnforcer interface { 291 EnforceErr(rvals ...interface{}) error 292 } 293 294 // Manager is the object that will be responsible for registering 295 // and handling proxy extensions. 296 type Manager struct { 297 log *log.Entry 298 settings SettingsGetter 299 application ApplicationGetter 300 project ProjectGetter 301 rbac RbacEnforcer 302 registry ExtensionRegistry 303 } 304 305 // NewManager will initialize a new manager. 306 func NewManager(log *log.Entry, sg SettingsGetter, ag ApplicationGetter, pg ProjectGetter, rbac RbacEnforcer) *Manager { 307 return &Manager{ 308 log: log, 309 settings: sg, 310 application: ag, 311 project: pg, 312 rbac: rbac, 313 } 314 } 315 316 // ExtensionRegistry is an in memory registry that contains contains all 317 // proxies for all extensions. The key is the extension name defined in 318 // the Argo CD configmap. 319 type ExtensionRegistry map[string]ProxyRegistry 320 321 // ProxyRegistry is an in memory registry that contains all proxies for a 322 // given extension. Different extensions will have independent proxy registries. 323 // This is required to address the use case when one extension is configured with 324 // multiple backend services in different clusters. 325 type ProxyRegistry map[ProxyKey]*httputil.ReverseProxy 326 327 // NewProxyRegistry will instantiate a new in memory registry for proxies. 328 func NewProxyRegistry() ProxyRegistry { 329 r := make(map[ProxyKey]*httputil.ReverseProxy) 330 return r 331 } 332 333 // ProxyKey defines the struct used as a key in the proxy registry 334 // map (ProxyRegistry). 335 type ProxyKey struct { 336 extensionName string 337 clusterName string 338 clusterServer string 339 } 340 341 // proxyKey will build the key to be used in the proxyByCluster 342 // map. 343 func proxyKey(extName, cName, cServer string) ProxyKey { 344 return ProxyKey{ 345 extensionName: extName, 346 clusterName: cName, 347 clusterServer: cServer, 348 } 349 } 350 351 func parseAndValidateConfig(s *settings.ArgoCDSettings) (*ExtensionConfigs, error) { 352 if s.ExtensionConfig == "" { 353 return nil, fmt.Errorf("no extensions configurations found") 354 } 355 356 extConfigMap := map[string]interface{}{} 357 err := yaml.Unmarshal([]byte(s.ExtensionConfig), &extConfigMap) 358 if err != nil { 359 return nil, fmt.Errorf("invalid extension config: %s", err) 360 } 361 362 parsedExtConfig := settings.ReplaceMapSecrets(extConfigMap, s.Secrets) 363 parsedExtConfigBytes, err := yaml.Marshal(parsedExtConfig) 364 if err != nil { 365 return nil, fmt.Errorf("error marshaling parsed extension config: %s", err) 366 } 367 368 configs := ExtensionConfigs{} 369 err = yaml.Unmarshal(parsedExtConfigBytes, &configs) 370 if err != nil { 371 return nil, fmt.Errorf("invalid parsed extension config: %s", err) 372 } 373 err = validateConfigs(&configs) 374 if err != nil { 375 return nil, fmt.Errorf("validation error: %s", err) 376 } 377 return &configs, nil 378 } 379 380 func validateConfigs(configs *ExtensionConfigs) error { 381 nameSafeRegex := regexp.MustCompile(`^[A-Za-z0-9-_]+$`) 382 exts := make(map[string]struct{}) 383 for _, ext := range configs.Extensions { 384 if ext.Name == "" { 385 return fmt.Errorf("extensions.name must be configured") 386 } 387 if !nameSafeRegex.MatchString(ext.Name) { 388 return fmt.Errorf("invalid extensions.name: only alphanumeric characters, hyphens, and underscores are allowed") 389 } 390 if _, found := exts[ext.Name]; found { 391 return fmt.Errorf("duplicated extension found in the configs for %q", ext.Name) 392 } 393 exts[ext.Name] = struct{}{} 394 svcTotal := len(ext.Backend.Services) 395 if svcTotal == 0 { 396 return fmt.Errorf("no backend service configured for extension %s", ext.Name) 397 } 398 for _, svc := range ext.Backend.Services { 399 if svc.URL == "" { 400 return fmt.Errorf("extensions.backend.services.url must be configured") 401 } 402 if svcTotal > 1 && svc.Cluster == nil { 403 return fmt.Errorf("extensions.backend.services.cluster must be configured when defining more than one service per extension") 404 } 405 if svc.Cluster != nil { 406 if svc.Cluster.Name == "" && svc.Cluster.Server == "" { 407 return fmt.Errorf("cluster.name or cluster.server must be defined when cluster is provided in the configuration") 408 } 409 } 410 if len(svc.Headers) > 0 { 411 for _, header := range svc.Headers { 412 if header.Name == "" { 413 return fmt.Errorf("header.name must be defined when providing service headers in the configuration") 414 } 415 if header.Value == "" { 416 return fmt.Errorf("header.value must be defined when providing service headers in the configuration") 417 } 418 } 419 } 420 } 421 } 422 return nil 423 } 424 425 // NewProxy will instantiate a new reverse proxy based on the provided 426 // targetURL and config. 427 func NewProxy(targetURL string, headers []Header, config ProxyConfig) (*httputil.ReverseProxy, error) { 428 url, err := url.Parse(targetURL) 429 if err != nil { 430 return nil, fmt.Errorf("failed to parse proxy URL: %s", err) 431 } 432 proxy := &httputil.ReverseProxy{ 433 Transport: newTransport(config), 434 Director: func(req *http.Request) { 435 req.Host = url.Host 436 req.URL.Scheme = url.Scheme 437 req.URL.Host = url.Host 438 req.Header.Set("Host", url.Host) 439 req.Header.Del("Authorization") 440 req.Header.Del("Cookie") 441 for _, header := range headers { 442 req.Header.Set(header.Name, header.Value) 443 } 444 }, 445 } 446 return proxy, nil 447 } 448 449 // newTransport will build a new transport to be used in the proxy 450 // applying default values if not defined in the given config. 451 func newTransport(config ProxyConfig) *http.Transport { 452 applyProxyConfigDefaults(&config) 453 return &http.Transport{ 454 DialContext: (&net.Dialer{ 455 Timeout: config.ConnectionTimeout, 456 KeepAlive: config.KeepAlive, 457 }).DialContext, 458 MaxIdleConns: config.MaxIdleConnections, 459 IdleConnTimeout: config.IdleConnectionTimeout, 460 TLSHandshakeTimeout: 10 * time.Second, 461 ExpectContinueTimeout: 1 * time.Second, 462 } 463 } 464 465 func applyProxyConfigDefaults(c *ProxyConfig) { 466 if c.ConnectionTimeout == 0 { 467 c.ConnectionTimeout = DefaultConnectionTimeout 468 } 469 if c.KeepAlive == 0 { 470 c.KeepAlive = DefaultKeepAlive 471 } 472 if c.IdleConnectionTimeout == 0 { 473 c.IdleConnectionTimeout = DefaultIdleConnectionTimeout 474 } 475 if c.MaxIdleConnections == 0 { 476 c.MaxIdleConnections = DefaultMaxIdleConnections 477 } 478 } 479 480 // RegisterExtensions will retrieve all extensions configurations 481 // and update the extension registry. 482 func (m *Manager) RegisterExtensions() error { 483 settings, err := m.settings.Get() 484 if err != nil { 485 return fmt.Errorf("error getting settings: %s", err) 486 } 487 err = m.UpdateExtensionRegistry(settings) 488 if err != nil { 489 return fmt.Errorf("error updating extension registry: %s", err) 490 } 491 return nil 492 } 493 494 // UpdateExtensionRegistry will first parse and validate the extensions 495 // configurations from the given settings. If no errors are found, it will 496 // iterate over the given configurations building a new extension registry. 497 // At the end, it will update the manager with the newly created registry. 498 func (m *Manager) UpdateExtensionRegistry(s *settings.ArgoCDSettings) error { 499 extConfigs, err := parseAndValidateConfig(s) 500 if err != nil { 501 return fmt.Errorf("error parsing extension config: %s", err) 502 } 503 extReg := make(map[string]ProxyRegistry) 504 for _, ext := range extConfigs.Extensions { 505 proxyReg := NewProxyRegistry() 506 singleBackend := len(ext.Backend.Services) == 1 507 for _, service := range ext.Backend.Services { 508 proxy, err := NewProxy(service.URL, service.Headers, ext.Backend.ProxyConfig) 509 if err != nil { 510 return fmt.Errorf("error creating proxy: %s", err) 511 } 512 err = appendProxy(proxyReg, ext.Name, service, proxy, singleBackend) 513 if err != nil { 514 return fmt.Errorf("error appending proxy: %s", err) 515 } 516 } 517 extReg[ext.Name] = proxyReg 518 } 519 m.registry = extReg 520 return nil 521 } 522 523 // appendProxy will append the given proxy in the given registry. Will use 524 // the provided extName and service to determine the map key. The key must 525 // be unique in the map. If the map already has the key and error is returned. 526 func appendProxy(registry ProxyRegistry, 527 extName string, 528 service ServiceConfig, 529 proxy *httputil.ReverseProxy, 530 singleBackend bool) error { 531 532 if singleBackend { 533 key := proxyKey(extName, "", "") 534 if _, exist := registry[key]; exist { 535 return fmt.Errorf("duplicated proxy configuration found for extension key %q", key) 536 } 537 registry[key] = proxy 538 return nil 539 } 540 541 // This is the case where there are more than one backend configured 542 // for this extension. In this case we need to add the provided cluster 543 // configurations for proper correlation to find which proxy to use 544 // while handling requests. 545 if service.Cluster.Name != "" { 546 key := proxyKey(extName, service.Cluster.Name, "") 547 if _, exist := registry[key]; exist { 548 return fmt.Errorf("duplicated proxy configuration found for extension key %q", key) 549 } 550 registry[key] = proxy 551 } 552 if service.Cluster.Server != "" { 553 key := proxyKey(extName, "", service.Cluster.Server) 554 if _, exist := registry[key]; exist { 555 return fmt.Errorf("duplicated proxy configuration found for extension key %q", key) 556 } 557 registry[key] = proxy 558 } 559 return nil 560 } 561 562 // authorize will enforce rbac rules are satified for the given RequestResources. 563 // The following validations are executed: 564 // - enforce the subject has permission to read application/project provided 565 // in HeaderArgoCDApplicationName and HeaderArgoCDProjectName. 566 // - enforce the subject has permission to invoke the extension identified by 567 // extName. 568 // - enforce that the project has permission to access the destination cluster. 569 // 570 // If all validations are satified it will return the Application resource 571 func (m *Manager) authorize(ctx context.Context, rr *RequestResources, extName string) (*v1alpha1.Application, error) { 572 if m.rbac == nil { 573 return nil, fmt.Errorf("rbac enforcer not set in extension manager") 574 } 575 appRBACName := security.RBACName(rr.ApplicationNamespace, rr.ProjectName, rr.ApplicationNamespace, rr.ApplicationName) 576 if err := m.rbac.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, appRBACName); err != nil { 577 return nil, fmt.Errorf("application authorization error: %s", err) 578 } 579 580 if err := m.rbac.EnforceErr(ctx.Value("claims"), rbacpolicy.ResourceExtensions, rbacpolicy.ActionInvoke, extName); err != nil { 581 return nil, fmt.Errorf("unauthorized to invoke extension %q: %s", extName, err) 582 } 583 584 // just retrieve the app after checking if subject has access to it 585 app, err := m.application.Get(rr.ApplicationNamespace, rr.ApplicationName) 586 if err != nil { 587 return nil, fmt.Errorf("error getting application: %s", err) 588 } 589 if app == nil { 590 return nil, fmt.Errorf("invalid Application provided in the %q header", HeaderArgoCDApplicationName) 591 } 592 593 if app.Spec.GetProject() != rr.ProjectName { 594 return nil, fmt.Errorf("project mismatch provided in the %q header", HeaderArgoCDProjectName) 595 } 596 597 proj, err := m.project.Get(app.Spec.GetProject()) 598 if err != nil { 599 return nil, fmt.Errorf("error getting project: %s", err) 600 } 601 if proj == nil { 602 return nil, fmt.Errorf("invalid project provided in the %q header", HeaderArgoCDProjectName) 603 } 604 permitted, err := proj.IsDestinationPermitted(app.Spec.Destination, m.project.GetClusters) 605 if err != nil { 606 return nil, fmt.Errorf("error validating project destinations: %s", err) 607 } 608 if !permitted { 609 return nil, fmt.Errorf("the provided project is not allowed to access the cluster configured in the Application destination") 610 } 611 612 return app, nil 613 } 614 615 // findProxy will search the given registry to find the correct proxy to use 616 // based on the given extName and dest. 617 func findProxy(registry ProxyRegistry, extName string, dest v1alpha1.ApplicationDestination) (*httputil.ReverseProxy, error) { 618 619 // First try to find the proxy in the registry just by the extension name. 620 // This is the simple case for extensions with only one backend service. 621 key := proxyKey(extName, "", "") 622 if proxy, found := registry[key]; found { 623 return proxy, nil 624 } 625 626 // If extension has multiple backend services configured, the correct proxy 627 // needs to be searched by the ApplicationDestination. 628 key = proxyKey(extName, dest.Name, dest.Server) 629 if proxy, found := registry[key]; found { 630 return proxy, nil 631 } 632 633 return nil, fmt.Errorf("no proxy found for extension %q", extName) 634 } 635 636 // ProxyRegistry returns the proxy registry associated for the given 637 // extension name. 638 func (m *Manager) ProxyRegistry(name string) (ProxyRegistry, bool) { 639 pReg, found := m.registry[name] 640 return pReg, found 641 } 642 643 // CallExtension returns a handler func responsible for forwarding requests to the 644 // extension service. The request will be sanitized by removing sensitive headers. 645 func (m *Manager) CallExtension() func(http.ResponseWriter, *http.Request) { 646 return func(w http.ResponseWriter, r *http.Request) { 647 segments := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/") 648 if segments[0] != "extensions" { 649 http.Error(w, fmt.Sprintf("Invalid URL: first segment must be %s", URLPrefix), http.StatusBadRequest) 650 return 651 } 652 extName := segments[1] 653 if extName == "" { 654 http.Error(w, "Invalid URL: extension name must be provided", http.StatusBadRequest) 655 return 656 } 657 extName = strings.ReplaceAll(extName, "\n", "") 658 extName = strings.ReplaceAll(extName, "\r", "") 659 reqResources, err := ValidateHeaders(r) 660 if err != nil { 661 http.Error(w, fmt.Sprintf("Invalid headers: %s", err), http.StatusBadRequest) 662 return 663 } 664 app, err := m.authorize(r.Context(), reqResources, extName) 665 if err != nil { 666 m.log.Infof("unauthorized extension request: %s", err) 667 http.Error(w, "Unauthorized extension request", http.StatusUnauthorized) 668 return 669 } 670 671 proxyRegistry, ok := m.ProxyRegistry(extName) 672 if !ok { 673 m.log.Warnf("proxy extension warning: attempt to call unregistered extension: %s", extName) 674 http.Error(w, "Extension not found", http.StatusNotFound) 675 return 676 } 677 proxy, err := findProxy(proxyRegistry, extName, app.Spec.Destination) 678 if err != nil { 679 m.log.Errorf("findProxy error: %s", err) 680 http.Error(w, "invalid extension", http.StatusBadRequest) 681 return 682 } 683 684 prepareRequest(r, extName, app) 685 m.log.Debugf("proxing request for extension %q", extName) 686 proxy.ServeHTTP(w, r) 687 } 688 } 689 690 // prepareRequest is reponsible for preparing and cleaning the given 691 // request, removing sensitive information before forwarding it to the 692 // proxy extension. 693 func prepareRequest(r *http.Request, extName string, app *v1alpha1.Application) { 694 r.URL.Path = strings.TrimPrefix(r.URL.Path, fmt.Sprintf("%s/%s", URLPrefix, extName)) 695 if app.Spec.Destination.Name != "" { 696 r.Header.Set(HeaderArgoCDTargetClusterName, app.Spec.Destination.Name) 697 } 698 if app.Spec.Destination.Server != "" { 699 r.Header.Set(HeaderArgoCDTargetClusterURL, app.Spec.Destination.Server) 700 } 701 }