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  }