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