github.com/argoproj/argo-cd/v2@v2.10.9/server/server.go (about) 1 package server 2 3 import ( 4 "context" 5 "crypto/tls" 6 "errors" 7 "fmt" 8 goio "io" 9 "io/fs" 10 "math" 11 "net" 12 "net/http" 13 "net/url" 14 "os" 15 "os/exec" 16 "path" 17 "path/filepath" 18 "reflect" 19 "regexp" 20 go_runtime "runtime" 21 "strings" 22 gosync "sync" 23 "time" 24 25 // nolint:staticcheck 26 golang_proto "github.com/golang/protobuf/proto" 27 "k8s.io/apimachinery/pkg/labels" 28 "k8s.io/apimachinery/pkg/selection" 29 30 "github.com/argoproj/notifications-engine/pkg/api" 31 "github.com/argoproj/pkg/sync" 32 "github.com/golang-jwt/jwt/v4" 33 "github.com/gorilla/handlers" 34 grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" 35 grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" 36 grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" 37 grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 38 "github.com/grpc-ecosystem/grpc-gateway/runtime" 39 "github.com/improbable-eng/grpc-web/go/grpcweb" 40 "github.com/redis/go-redis/v9" 41 log "github.com/sirupsen/logrus" 42 "github.com/soheilhy/cmux" 43 "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 44 "google.golang.org/grpc" 45 "google.golang.org/grpc/codes" 46 "google.golang.org/grpc/credentials" 47 "google.golang.org/grpc/credentials/insecure" 48 "google.golang.org/grpc/keepalive" 49 "google.golang.org/grpc/metadata" 50 "google.golang.org/grpc/reflection" 51 "google.golang.org/grpc/status" 52 "gopkg.in/yaml.v2" 53 v1 "k8s.io/api/core/v1" 54 apierrors "k8s.io/apimachinery/pkg/api/errors" 55 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 56 "k8s.io/apimachinery/pkg/util/wait" 57 "k8s.io/client-go/kubernetes" 58 "k8s.io/client-go/tools/cache" 59 60 "github.com/argoproj/argo-cd/v2/common" 61 "github.com/argoproj/argo-cd/v2/pkg/apiclient" 62 accountpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/account" 63 applicationpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/application" 64 applicationsetpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/applicationset" 65 certificatepkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/certificate" 66 clusterpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster" 67 gpgkeypkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/gpgkey" 68 notificationpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/notification" 69 projectpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/project" 70 repocredspkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/repocreds" 71 repositorypkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/repository" 72 sessionpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/session" 73 settingspkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/settings" 74 versionpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/version" 75 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 76 appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" 77 appinformer "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions" 78 applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1" 79 repoapiclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient" 80 repocache "github.com/argoproj/argo-cd/v2/reposerver/cache" 81 "github.com/argoproj/argo-cd/v2/server/account" 82 "github.com/argoproj/argo-cd/v2/server/application" 83 "github.com/argoproj/argo-cd/v2/server/applicationset" 84 "github.com/argoproj/argo-cd/v2/server/badge" 85 servercache "github.com/argoproj/argo-cd/v2/server/cache" 86 "github.com/argoproj/argo-cd/v2/server/certificate" 87 "github.com/argoproj/argo-cd/v2/server/cluster" 88 "github.com/argoproj/argo-cd/v2/server/extension" 89 "github.com/argoproj/argo-cd/v2/server/gpgkey" 90 "github.com/argoproj/argo-cd/v2/server/logout" 91 "github.com/argoproj/argo-cd/v2/server/metrics" 92 "github.com/argoproj/argo-cd/v2/server/notification" 93 "github.com/argoproj/argo-cd/v2/server/project" 94 "github.com/argoproj/argo-cd/v2/server/rbacpolicy" 95 "github.com/argoproj/argo-cd/v2/server/repocreds" 96 "github.com/argoproj/argo-cd/v2/server/repository" 97 "github.com/argoproj/argo-cd/v2/server/session" 98 "github.com/argoproj/argo-cd/v2/server/settings" 99 "github.com/argoproj/argo-cd/v2/server/version" 100 "github.com/argoproj/argo-cd/v2/ui" 101 "github.com/argoproj/argo-cd/v2/util/assets" 102 cacheutil "github.com/argoproj/argo-cd/v2/util/cache" 103 "github.com/argoproj/argo-cd/v2/util/db" 104 dexutil "github.com/argoproj/argo-cd/v2/util/dex" 105 "github.com/argoproj/argo-cd/v2/util/env" 106 errorsutil "github.com/argoproj/argo-cd/v2/util/errors" 107 grpc_util "github.com/argoproj/argo-cd/v2/util/grpc" 108 "github.com/argoproj/argo-cd/v2/util/healthz" 109 httputil "github.com/argoproj/argo-cd/v2/util/http" 110 "github.com/argoproj/argo-cd/v2/util/io" 111 "github.com/argoproj/argo-cd/v2/util/io/files" 112 jwtutil "github.com/argoproj/argo-cd/v2/util/jwt" 113 kubeutil "github.com/argoproj/argo-cd/v2/util/kube" 114 service "github.com/argoproj/argo-cd/v2/util/notification/argocd" 115 "github.com/argoproj/argo-cd/v2/util/notification/k8s" 116 settings_notif "github.com/argoproj/argo-cd/v2/util/notification/settings" 117 "github.com/argoproj/argo-cd/v2/util/oidc" 118 "github.com/argoproj/argo-cd/v2/util/rbac" 119 util_session "github.com/argoproj/argo-cd/v2/util/session" 120 settings_util "github.com/argoproj/argo-cd/v2/util/settings" 121 "github.com/argoproj/argo-cd/v2/util/swagger" 122 tlsutil "github.com/argoproj/argo-cd/v2/util/tls" 123 "github.com/argoproj/argo-cd/v2/util/webhook" 124 ) 125 126 const maxConcurrentLoginRequestsCountEnv = "ARGOCD_MAX_CONCURRENT_LOGIN_REQUESTS_COUNT" 127 const replicasCountEnv = "ARGOCD_API_SERVER_REPLICAS" 128 const renewTokenKey = "renew-token" 129 130 // ErrNoSession indicates no auth token was supplied as part of a request 131 var ErrNoSession = status.Errorf(codes.Unauthenticated, "no session information") 132 133 var noCacheHeaders = map[string]string{ 134 "Expires": time.Unix(0, 0).Format(time.RFC1123), 135 "Cache-Control": "no-cache, private, max-age=0", 136 "Pragma": "no-cache", 137 "X-Accel-Expires": "0", 138 } 139 140 var backoff = wait.Backoff{ 141 Steps: 5, 142 Duration: 500 * time.Millisecond, 143 Factor: 1.0, 144 Jitter: 0.1, 145 } 146 147 var ( 148 clientConstraint = fmt.Sprintf(">= %s", common.MinClientVersion) 149 baseHRefRegex = regexp.MustCompile(`<base href="(.*?)">`) 150 // limits number of concurrent login requests to prevent password brute forcing. If set to 0 then no limit is enforced. 151 maxConcurrentLoginRequestsCount = 50 152 replicasCount = 1 153 enableGRPCTimeHistogram = true 154 ) 155 156 func init() { 157 maxConcurrentLoginRequestsCount = env.ParseNumFromEnv(maxConcurrentLoginRequestsCountEnv, maxConcurrentLoginRequestsCount, 0, math.MaxInt32) 158 replicasCount = env.ParseNumFromEnv(replicasCountEnv, replicasCount, 0, math.MaxInt32) 159 if replicasCount > 0 { 160 maxConcurrentLoginRequestsCount = maxConcurrentLoginRequestsCount / replicasCount 161 } 162 enableGRPCTimeHistogram = env.ParseBoolFromEnv(common.EnvEnableGRPCTimeHistogramEnv, false) 163 } 164 165 // ArgoCDServer is the API server for Argo CD 166 type ArgoCDServer struct { 167 ArgoCDServerOpts 168 169 ssoClientApp *oidc.ClientApp 170 settings *settings_util.ArgoCDSettings 171 log *log.Entry 172 sessionMgr *util_session.SessionManager 173 settingsMgr *settings_util.SettingsManager 174 enf *rbac.Enforcer 175 projInformer cache.SharedIndexInformer 176 projLister applisters.AppProjectNamespaceLister 177 policyEnforcer *rbacpolicy.RBACPolicyEnforcer 178 appInformer cache.SharedIndexInformer 179 appLister applisters.ApplicationLister 180 appsetInformer cache.SharedIndexInformer 181 appsetLister applisters.ApplicationSetLister 182 db db.ArgoDB 183 184 // stopCh is the channel which when closed, will shutdown the Argo CD server 185 stopCh chan struct{} 186 userStateStorage util_session.UserStateStorage 187 indexDataInit gosync.Once 188 indexData []byte 189 indexDataErr error 190 staticAssets http.FileSystem 191 apiFactory api.Factory 192 secretInformer cache.SharedIndexInformer 193 configMapInformer cache.SharedIndexInformer 194 serviceSet *ArgoCDServiceSet 195 extensionManager *extension.Manager 196 } 197 198 type ArgoCDServerOpts struct { 199 DisableAuth bool 200 ContentTypes []string 201 EnableGZip bool 202 Insecure bool 203 StaticAssetsDir string 204 ListenPort int 205 ListenHost string 206 MetricsPort int 207 MetricsHost string 208 Namespace string 209 DexServerAddr string 210 DexTLSConfig *dexutil.DexTLSConfig 211 BaseHRef string 212 RootPath string 213 KubeClientset kubernetes.Interface 214 AppClientset appclientset.Interface 215 RepoClientset repoapiclient.Clientset 216 Cache *servercache.Cache 217 RedisClient *redis.Client 218 TLSConfigCustomizer tlsutil.ConfigCustomizer 219 XFrameOptions string 220 ContentSecurityPolicy string 221 ApplicationNamespaces []string 222 EnableProxyExtension bool 223 } 224 225 // initializeDefaultProject creates the default project if it does not already exist 226 func initializeDefaultProject(opts ArgoCDServerOpts) error { 227 defaultProj := &v1alpha1.AppProject{ 228 ObjectMeta: metav1.ObjectMeta{Name: v1alpha1.DefaultAppProjectName, Namespace: opts.Namespace}, 229 Spec: v1alpha1.AppProjectSpec{ 230 SourceRepos: []string{"*"}, 231 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 232 ClusterResourceWhitelist: []metav1.GroupKind{{Group: "*", Kind: "*"}}, 233 }, 234 } 235 236 _, err := opts.AppClientset.ArgoprojV1alpha1().AppProjects(opts.Namespace).Get(context.Background(), defaultProj.Name, metav1.GetOptions{}) 237 if apierrors.IsNotFound(err) { 238 _, err = opts.AppClientset.ArgoprojV1alpha1().AppProjects(opts.Namespace).Create(context.Background(), defaultProj, metav1.CreateOptions{}) 239 if apierrors.IsAlreadyExists(err) { 240 return nil 241 } 242 } 243 return err 244 } 245 246 // NewServer returns a new instance of the Argo CD API server 247 func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer { 248 settingsMgr := settings_util.NewSettingsManager(ctx, opts.KubeClientset, opts.Namespace) 249 settings, err := settingsMgr.InitializeSettings(opts.Insecure) 250 errorsutil.CheckError(err) 251 err = initializeDefaultProject(opts) 252 errorsutil.CheckError(err) 253 254 appInformerNs := opts.Namespace 255 if len(opts.ApplicationNamespaces) > 0 { 256 appInformerNs = "" 257 } 258 projFactory := appinformer.NewSharedInformerFactoryWithOptions(opts.AppClientset, 0, appinformer.WithNamespace(opts.Namespace), appinformer.WithTweakListOptions(func(options *metav1.ListOptions) {})) 259 appFactory := appinformer.NewSharedInformerFactoryWithOptions(opts.AppClientset, 0, appinformer.WithNamespace(appInformerNs), appinformer.WithTweakListOptions(func(options *metav1.ListOptions) {})) 260 261 projInformer := projFactory.Argoproj().V1alpha1().AppProjects().Informer() 262 projLister := projFactory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(opts.Namespace) 263 264 appInformer := appFactory.Argoproj().V1alpha1().Applications().Informer() 265 appLister := appFactory.Argoproj().V1alpha1().Applications().Lister() 266 267 appsetInformer := appFactory.Argoproj().V1alpha1().ApplicationSets().Informer() 268 appsetLister := appFactory.Argoproj().V1alpha1().ApplicationSets().Lister() 269 270 userStateStorage := util_session.NewUserStateStorage(opts.RedisClient) 271 sessionMgr := util_session.NewSessionManager(settingsMgr, projLister, opts.DexServerAddr, opts.DexTLSConfig, userStateStorage) 272 enf := rbac.NewEnforcer(opts.KubeClientset, opts.Namespace, common.ArgoCDRBACConfigMapName, nil) 273 enf.EnableEnforce(!opts.DisableAuth) 274 err = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 275 errorsutil.CheckError(err) 276 enf.EnableLog(os.Getenv(common.EnvVarRBACDebug) == "1") 277 278 policyEnf := rbacpolicy.NewRBACPolicyEnforcer(enf, projLister) 279 enf.SetClaimsEnforcerFunc(policyEnf.EnforceClaims) 280 281 var staticFS fs.FS = io.NewSubDirFS("dist/app", ui.Embedded) 282 if opts.StaticAssetsDir != "" { 283 staticFS = io.NewComposableFS(staticFS, os.DirFS(opts.StaticAssetsDir)) 284 } 285 286 argocdService, err := service.NewArgoCDService(opts.KubeClientset, opts.Namespace, opts.RepoClientset) 287 errorsutil.CheckError(err) 288 289 secretInformer := k8s.NewSecretInformer(opts.KubeClientset, opts.Namespace, "argocd-notifications-secret") 290 configMapInformer := k8s.NewConfigMapInformer(opts.KubeClientset, opts.Namespace, "argocd-notifications-cm") 291 292 apiFactory := api.NewFactory(settings_notif.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm", false), opts.Namespace, secretInformer, configMapInformer) 293 294 dbInstance := db.NewDB(opts.Namespace, settingsMgr, opts.KubeClientset) 295 logger := log.NewEntry(log.StandardLogger()) 296 297 sg := extension.NewDefaultSettingsGetter(settingsMgr) 298 ag := extension.NewDefaultApplicationGetter(appLister) 299 pg := extension.NewDefaultProjectGetter(projLister, dbInstance) 300 em := extension.NewManager(logger, sg, ag, pg, enf) 301 302 a := &ArgoCDServer{ 303 ArgoCDServerOpts: opts, 304 log: logger, 305 settings: settings, 306 sessionMgr: sessionMgr, 307 settingsMgr: settingsMgr, 308 enf: enf, 309 projInformer: projInformer, 310 projLister: projLister, 311 appInformer: appInformer, 312 appLister: appLister, 313 appsetInformer: appsetInformer, 314 appsetLister: appsetLister, 315 policyEnforcer: policyEnf, 316 userStateStorage: userStateStorage, 317 staticAssets: http.FS(staticFS), 318 db: dbInstance, 319 apiFactory: apiFactory, 320 secretInformer: secretInformer, 321 configMapInformer: configMapInformer, 322 extensionManager: em, 323 } 324 325 err = a.logInClusterWarnings() 326 if err != nil { 327 // Just log. It's not critical. 328 log.Warnf("Failed to log in-cluster warnings: %v", err) 329 } 330 331 return a 332 } 333 334 const ( 335 // catches corrupted informer state; see https://github.com/argoproj/argo-cd/issues/4960 for more information 336 notObjectErrMsg = "object does not implement the Object interfaces" 337 ) 338 339 func (a *ArgoCDServer) healthCheck(r *http.Request) error { 340 if val, ok := r.URL.Query()["full"]; ok && len(val) > 0 && val[0] == "true" { 341 argoDB := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset) 342 _, err := argoDB.ListClusters(r.Context()) 343 if err != nil && strings.Contains(err.Error(), notObjectErrMsg) { 344 return err 345 } 346 } 347 return nil 348 } 349 350 type Listeners struct { 351 Main net.Listener 352 Metrics net.Listener 353 GatewayConn *grpc.ClientConn 354 } 355 356 func (l *Listeners) Close() error { 357 if l.Main != nil { 358 if err := l.Main.Close(); err != nil { 359 return err 360 } 361 l.Main = nil 362 } 363 if l.Metrics != nil { 364 if err := l.Metrics.Close(); err != nil { 365 return err 366 } 367 l.Metrics = nil 368 } 369 if l.GatewayConn != nil { 370 if err := l.GatewayConn.Close(); err != nil { 371 return err 372 } 373 l.GatewayConn = nil 374 } 375 return nil 376 } 377 378 // logInClusterWarnings checks the in-cluster configuration and prints out any warnings. 379 func (a *ArgoCDServer) logInClusterWarnings() error { 380 labelSelector := labels.NewSelector() 381 req, err := labels.NewRequirement(common.LabelKeySecretType, selection.Equals, []string{common.LabelValueSecretTypeCluster}) 382 if err != nil { 383 return fmt.Errorf("failed to construct cluster-type label selector: %w", err) 384 } 385 labelSelector = labelSelector.Add(*req) 386 secretsLister, err := a.settingsMgr.GetSecretsLister() 387 if err != nil { 388 return fmt.Errorf("failed to get secrets lister: %w", err) 389 } 390 clusterSecrets, err := secretsLister.Secrets(a.ArgoCDServerOpts.Namespace).List(labelSelector) 391 if err != nil { 392 return fmt.Errorf("failed to list cluster secrets: %w", err) 393 } 394 var inClusterSecrets []string 395 for _, clusterSecret := range clusterSecrets { 396 cluster, err := db.SecretToCluster(clusterSecret) 397 if err != nil { 398 return fmt.Errorf("could not unmarshal cluster secret %q: %w", clusterSecret.Name, err) 399 } 400 if cluster.Server == v1alpha1.KubernetesInternalAPIServerAddr { 401 inClusterSecrets = append(inClusterSecrets, clusterSecret.Name) 402 } 403 } 404 if len(inClusterSecrets) > 0 { 405 // Don't make this call unless we actually have in-cluster secrets, to save time. 406 dbSettings, err := a.settingsMgr.GetSettings() 407 if err != nil { 408 return fmt.Errorf("could not get DB settings: %w", err) 409 } 410 if !dbSettings.InClusterEnabled { 411 for _, clusterName := range inClusterSecrets { 412 log.Warnf("cluster %q uses in-cluster server address but it's disabled in Argo CD settings", clusterName) 413 } 414 } 415 } 416 return nil 417 } 418 419 func startListener(host string, port int) (net.Listener, error) { 420 var conn net.Listener 421 var realErr error 422 _ = wait.ExponentialBackoff(backoff, func() (bool, error) { 423 conn, realErr = net.Listen("tcp", fmt.Sprintf("%s:%d", host, port)) 424 if realErr != nil { 425 return false, nil 426 } 427 return true, nil 428 }) 429 return conn, realErr 430 } 431 432 func (a *ArgoCDServer) Listen() (*Listeners, error) { 433 mainLn, err := startListener(a.ListenHost, a.ListenPort) 434 if err != nil { 435 return nil, err 436 } 437 metricsLn, err := startListener(a.ListenHost, a.MetricsPort) 438 if err != nil { 439 io.Close(mainLn) 440 return nil, err 441 } 442 var dOpts []grpc.DialOption 443 dOpts = append(dOpts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(apiclient.MaxGRPCMessageSize))) 444 dOpts = append(dOpts, grpc.WithUserAgent(fmt.Sprintf("%s/%s", common.ArgoCDUserAgentName, common.GetVersion().Version))) 445 dOpts = append(dOpts, grpc.WithUnaryInterceptor(grpc_util.OTELUnaryClientInterceptor())) 446 dOpts = append(dOpts, grpc.WithStreamInterceptor(grpc_util.OTELStreamClientInterceptor())) 447 if a.useTLS() { 448 // The following sets up the dial Options for grpc-gateway to talk to gRPC server over TLS. 449 // grpc-gateway is just translating HTTP/HTTPS requests as gRPC requests over localhost, 450 // so we need to supply the same certificates to establish the connections that a normal, 451 // external gRPC client would need. 452 tlsConfig := a.settings.TLSConfig() 453 if a.TLSConfigCustomizer != nil { 454 a.TLSConfigCustomizer(tlsConfig) 455 } 456 tlsConfig.InsecureSkipVerify = true 457 dCreds := credentials.NewTLS(tlsConfig) 458 dOpts = append(dOpts, grpc.WithTransportCredentials(dCreds)) 459 } else { 460 dOpts = append(dOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) 461 } 462 conn, err := grpc.Dial(fmt.Sprintf("localhost:%d", a.ListenPort), dOpts...) 463 if err != nil { 464 io.Close(mainLn) 465 io.Close(metricsLn) 466 return nil, err 467 } 468 return &Listeners{Main: mainLn, Metrics: metricsLn, GatewayConn: conn}, nil 469 } 470 471 // Init starts informers used by the API server 472 func (a *ArgoCDServer) Init(ctx context.Context) { 473 go a.projInformer.Run(ctx.Done()) 474 go a.appInformer.Run(ctx.Done()) 475 go a.appsetInformer.Run(ctx.Done()) 476 go a.configMapInformer.Run(ctx.Done()) 477 go a.secretInformer.Run(ctx.Done()) 478 } 479 480 // Run runs the API Server 481 // We use k8s.io/code-generator/cmd/go-to-protobuf to generate the .proto files from the API types. 482 // k8s.io/ go-to-protobuf uses protoc-gen-gogo, which comes from gogo/protobuf (a fork of 483 // golang/protobuf). 484 func (a *ArgoCDServer) Run(ctx context.Context, listeners *Listeners) { 485 a.userStateStorage.Init(ctx) 486 svcSet := newArgoCDServiceSet(a) 487 a.serviceSet = svcSet 488 grpcS, appResourceTreeFn := a.newGRPCServer() 489 grpcWebS := grpcweb.WrapServer(grpcS) 490 var httpS *http.Server 491 var httpsS *http.Server 492 if a.useTLS() { 493 httpS = newRedirectServer(a.ListenPort, a.RootPath) 494 httpsS = a.newHTTPServer(ctx, a.ListenPort, grpcWebS, appResourceTreeFn, listeners.GatewayConn) 495 } else { 496 httpS = a.newHTTPServer(ctx, a.ListenPort, grpcWebS, appResourceTreeFn, listeners.GatewayConn) 497 } 498 if a.RootPath != "" { 499 httpS.Handler = withRootPath(httpS.Handler, a) 500 501 if httpsS != nil { 502 httpsS.Handler = withRootPath(httpsS.Handler, a) 503 } 504 } 505 httpS.Handler = &bug21955Workaround{handler: httpS.Handler} 506 if httpsS != nil { 507 httpsS.Handler = &bug21955Workaround{handler: httpsS.Handler} 508 } 509 510 metricsServ := metrics.NewMetricsServer(a.MetricsHost, a.MetricsPort) 511 if a.RedisClient != nil { 512 cacheutil.CollectMetrics(a.RedisClient, metricsServ) 513 } 514 515 // CMux is used to support servicing gRPC and HTTP1.1+JSON on the same port 516 tcpm := cmux.New(listeners.Main) 517 var tlsm cmux.CMux 518 var grpcL net.Listener 519 var httpL net.Listener 520 var httpsL net.Listener 521 if !a.useTLS() { 522 httpL = tcpm.Match(cmux.HTTP1Fast("PATCH")) 523 grpcL = tcpm.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")) 524 525 } else { 526 // We first match on HTTP 1.1 methods. 527 httpL = tcpm.Match(cmux.HTTP1Fast("PATCH")) 528 529 // If not matched, we assume that its TLS. 530 tlsl := tcpm.Match(cmux.Any()) 531 tlsConfig := tls.Config{} 532 tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { 533 return a.settings.Certificate, nil 534 } 535 if a.TLSConfigCustomizer != nil { 536 a.TLSConfigCustomizer(&tlsConfig) 537 } 538 tlsl = tls.NewListener(tlsl, &tlsConfig) 539 540 // Now, we build another mux recursively to match HTTPS and gRPC. 541 tlsm = cmux.New(tlsl) 542 httpsL = tlsm.Match(cmux.HTTP1Fast("PATCH")) 543 grpcL = tlsm.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")) 544 } 545 546 // Start the muxed listeners for our servers 547 log.Infof("argocd %s serving on port %d (url: %s, tls: %v, namespace: %s, sso: %v)", 548 common.GetVersion(), a.ListenPort, a.settings.URL, a.useTLS(), a.Namespace, a.settings.IsSSOConfigured()) 549 log.Infof("Enabled application namespace patterns: %s", a.allowedApplicationNamespacesAsString()) 550 551 go func() { a.checkServeErr("grpcS", grpcS.Serve(grpcL)) }() 552 go func() { a.checkServeErr("httpS", httpS.Serve(httpL)) }() 553 if a.useTLS() { 554 go func() { a.checkServeErr("httpsS", httpsS.Serve(httpsL)) }() 555 go func() { a.checkServeErr("tlsm", tlsm.Serve()) }() 556 } 557 go a.watchSettings() 558 go a.rbacPolicyLoader(ctx) 559 go func() { a.checkServeErr("tcpm", tcpm.Serve()) }() 560 go func() { a.checkServeErr("metrics", metricsServ.Serve(listeners.Metrics)) }() 561 if !cache.WaitForCacheSync(ctx.Done(), a.projInformer.HasSynced, a.appInformer.HasSynced) { 562 log.Fatal("Timed out waiting for project cache to sync") 563 } 564 565 a.stopCh = make(chan struct{}) 566 <-a.stopCh 567 } 568 569 func (a *ArgoCDServer) Initialized() bool { 570 return a.projInformer.HasSynced() && a.appInformer.HasSynced() 571 } 572 573 // checkServeErr checks the error from a .Serve() call to decide if it was a graceful shutdown 574 func (a *ArgoCDServer) checkServeErr(name string, err error) { 575 if err != nil { 576 if a.stopCh == nil { 577 // a nil stopCh indicates a graceful shutdown 578 log.Infof("graceful shutdown %s: %v", name, err) 579 } else { 580 log.Fatalf("%s: %v", name, err) 581 } 582 } else { 583 log.Infof("graceful shutdown %s", name) 584 } 585 } 586 587 // Shutdown stops the Argo CD server 588 func (a *ArgoCDServer) Shutdown() { 589 log.Info("Shut down requested") 590 stopCh := a.stopCh 591 a.stopCh = nil 592 if stopCh != nil { 593 close(stopCh) 594 } 595 } 596 597 func checkOIDCConfigChange(currentOIDCConfig *settings_util.OIDCConfig, newArgoCDSettings *settings_util.ArgoCDSettings) bool { 598 newOIDCConfig := newArgoCDSettings.OIDCConfig() 599 600 if (currentOIDCConfig != nil && newOIDCConfig == nil) || (currentOIDCConfig == nil && newOIDCConfig != nil) { 601 return true 602 } 603 604 if currentOIDCConfig != nil && newOIDCConfig != nil { 605 if !reflect.DeepEqual(*currentOIDCConfig, *newOIDCConfig) { 606 return true 607 } 608 } 609 610 return false 611 } 612 613 // watchSettings watches the configmap and secret for any setting updates that would warrant a 614 // restart of the API server. 615 func (a *ArgoCDServer) watchSettings() { 616 updateCh := make(chan *settings_util.ArgoCDSettings, 1) 617 a.settingsMgr.Subscribe(updateCh) 618 619 prevURL := a.settings.URL 620 prevOIDCConfig := a.settings.OIDCConfig() 621 prevDexCfgBytes, err := dexutil.GenerateDexConfigYAML(a.settings, a.DexTLSConfig == nil || a.DexTLSConfig.DisableTLS) 622 errorsutil.CheckError(err) 623 prevGitHubSecret := a.settings.WebhookGitHubSecret 624 prevGitLabSecret := a.settings.WebhookGitLabSecret 625 prevBitbucketUUID := a.settings.WebhookBitbucketUUID 626 prevBitbucketServerSecret := a.settings.WebhookBitbucketServerSecret 627 prevGogsSecret := a.settings.WebhookGogsSecret 628 prevExtConfig := a.settings.ExtensionConfig 629 var prevCert, prevCertKey string 630 if a.settings.Certificate != nil && !a.ArgoCDServerOpts.Insecure { 631 prevCert, prevCertKey = tlsutil.EncodeX509KeyPairString(*a.settings.Certificate) 632 } 633 634 for { 635 newSettings := <-updateCh 636 a.settings = newSettings 637 newDexCfgBytes, err := dexutil.GenerateDexConfigYAML(a.settings, a.DexTLSConfig == nil || a.DexTLSConfig.DisableTLS) 638 errorsutil.CheckError(err) 639 if string(newDexCfgBytes) != string(prevDexCfgBytes) { 640 log.Infof("dex config modified. restarting") 641 break 642 } 643 if checkOIDCConfigChange(prevOIDCConfig, a.settings) { 644 log.Infof("oidc config modified. restarting") 645 break 646 } 647 if prevURL != a.settings.URL { 648 log.Infof("url modified. restarting") 649 break 650 } 651 if prevGitHubSecret != a.settings.WebhookGitHubSecret { 652 log.Infof("github secret modified. restarting") 653 break 654 } 655 if prevGitLabSecret != a.settings.WebhookGitLabSecret { 656 log.Infof("gitlab secret modified. restarting") 657 break 658 } 659 if prevBitbucketUUID != a.settings.WebhookBitbucketUUID { 660 log.Infof("bitbucket uuid modified. restarting") 661 break 662 } 663 if prevBitbucketServerSecret != a.settings.WebhookBitbucketServerSecret { 664 log.Infof("bitbucket server secret modified. restarting") 665 break 666 } 667 if prevGogsSecret != a.settings.WebhookGogsSecret { 668 log.Infof("gogs secret modified. restarting") 669 break 670 } 671 if prevExtConfig != a.settings.ExtensionConfig { 672 prevExtConfig = a.settings.ExtensionConfig 673 log.Infof("extensions configs modified. Updating proxy registry...") 674 err := a.extensionManager.UpdateExtensionRegistry(a.settings) 675 if err != nil { 676 log.Errorf("error updating extensions configs: %s", err) 677 } else { 678 log.Info("extensions configs updated successfully") 679 } 680 } 681 if !a.ArgoCDServerOpts.Insecure { 682 var newCert, newCertKey string 683 if a.settings.Certificate != nil { 684 newCert, newCertKey = tlsutil.EncodeX509KeyPairString(*a.settings.Certificate) 685 } 686 if newCert != prevCert || newCertKey != prevCertKey { 687 log.Infof("tls certificate modified. reloading certificate") 688 // No need to break out of this loop since TlsConfig.GetCertificate will automagically reload the cert. 689 } 690 } 691 } 692 log.Info("shutting down settings watch") 693 a.Shutdown() 694 a.settingsMgr.Unsubscribe(updateCh) 695 close(updateCh) 696 } 697 698 func (a *ArgoCDServer) rbacPolicyLoader(ctx context.Context) { 699 err := a.enf.RunPolicyLoader(ctx, func(cm *v1.ConfigMap) error { 700 var scopes []string 701 if scopesStr, ok := cm.Data[rbac.ConfigMapScopesKey]; len(scopesStr) > 0 && ok { 702 scopes = make([]string, 0) 703 err := yaml.Unmarshal([]byte(scopesStr), &scopes) 704 if err != nil { 705 return err 706 } 707 } 708 709 a.policyEnforcer.SetScopes(scopes) 710 return nil 711 }) 712 errorsutil.CheckError(err) 713 } 714 715 func (a *ArgoCDServer) useTLS() bool { 716 if a.Insecure || a.settings.Certificate == nil { 717 return false 718 } 719 return true 720 } 721 722 func (a *ArgoCDServer) newGRPCServer() (*grpc.Server, application.AppResourceTreeFn) { 723 if enableGRPCTimeHistogram { 724 grpc_prometheus.EnableHandlingTimeHistogram() 725 } 726 727 sOpts := []grpc.ServerOption{ 728 // Set the both send and receive the bytes limit to be 100MB 729 // The proper way to achieve high performance is to have pagination 730 // while we work toward that, we can have high limit first 731 grpc.MaxRecvMsgSize(apiclient.MaxGRPCMessageSize), 732 grpc.MaxSendMsgSize(apiclient.MaxGRPCMessageSize), 733 grpc.ConnectionTimeout(300 * time.Second), 734 grpc.KeepaliveEnforcementPolicy( 735 keepalive.EnforcementPolicy{ 736 MinTime: common.GetGRPCKeepAliveEnforcementMinimum(), 737 }, 738 ), 739 } 740 sensitiveMethods := map[string]bool{ 741 "/cluster.ClusterService/Create": true, 742 "/cluster.ClusterService/Update": true, 743 "/session.SessionService/Create": true, 744 "/account.AccountService/UpdatePassword": true, 745 "/gpgkey.GPGKeyService/CreateGnuPGPublicKey": true, 746 "/repository.RepositoryService/Create": true, 747 "/repository.RepositoryService/Update": true, 748 "/repository.RepositoryService/CreateRepository": true, 749 "/repository.RepositoryService/UpdateRepository": true, 750 "/repository.RepositoryService/ValidateAccess": true, 751 "/repocreds.RepoCredsService/CreateRepositoryCredentials": true, 752 "/repocreds.RepoCredsService/UpdateRepositoryCredentials": true, 753 "/application.ApplicationService/PatchResource": true, 754 // Remove from logs both because the contents are sensitive and because they may be very large. 755 "/application.ApplicationService/GetManifestsWithFiles": true, 756 } 757 // NOTE: notice we do not configure the gRPC server here with TLS (e.g. grpc.Creds(creds)) 758 // This is because TLS handshaking occurs in cmux handling 759 sOpts = append(sOpts, grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( 760 otelgrpc.StreamServerInterceptor(), 761 grpc_logrus.StreamServerInterceptor(a.log), 762 grpc_prometheus.StreamServerInterceptor, 763 grpc_auth.StreamServerInterceptor(a.Authenticate), 764 grpc_util.UserAgentStreamServerInterceptor(common.ArgoCDUserAgentName, clientConstraint), 765 grpc_util.PayloadStreamServerInterceptor(a.log, true, func(ctx context.Context, fullMethodName string, servingObject interface{}) bool { 766 return !sensitiveMethods[fullMethodName] 767 }), 768 grpc_util.ErrorCodeK8sStreamServerInterceptor(), 769 grpc_util.ErrorCodeGitStreamServerInterceptor(), 770 grpc_util.PanicLoggerStreamServerInterceptor(a.log), 771 ))) 772 sOpts = append(sOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( 773 bug21955WorkaroundInterceptor, 774 otelgrpc.UnaryServerInterceptor(), 775 grpc_logrus.UnaryServerInterceptor(a.log), 776 grpc_prometheus.UnaryServerInterceptor, 777 grpc_auth.UnaryServerInterceptor(a.Authenticate), 778 grpc_util.UserAgentUnaryServerInterceptor(common.ArgoCDUserAgentName, clientConstraint), 779 grpc_util.PayloadUnaryServerInterceptor(a.log, true, func(ctx context.Context, fullMethodName string, servingObject interface{}) bool { 780 return !sensitiveMethods[fullMethodName] 781 }), 782 grpc_util.ErrorCodeK8sUnaryServerInterceptor(), 783 grpc_util.ErrorCodeGitUnaryServerInterceptor(), 784 grpc_util.PanicLoggerUnaryServerInterceptor(a.log), 785 ))) 786 grpcS := grpc.NewServer(sOpts...) 787 788 versionpkg.RegisterVersionServiceServer(grpcS, a.serviceSet.VersionService) 789 clusterpkg.RegisterClusterServiceServer(grpcS, a.serviceSet.ClusterService) 790 applicationpkg.RegisterApplicationServiceServer(grpcS, a.serviceSet.ApplicationService) 791 applicationsetpkg.RegisterApplicationSetServiceServer(grpcS, a.serviceSet.ApplicationSetService) 792 notificationpkg.RegisterNotificationServiceServer(grpcS, a.serviceSet.NotificationService) 793 repositorypkg.RegisterRepositoryServiceServer(grpcS, a.serviceSet.RepoService) 794 repocredspkg.RegisterRepoCredsServiceServer(grpcS, a.serviceSet.RepoCredsService) 795 sessionpkg.RegisterSessionServiceServer(grpcS, a.serviceSet.SessionService) 796 settingspkg.RegisterSettingsServiceServer(grpcS, a.serviceSet.SettingsService) 797 projectpkg.RegisterProjectServiceServer(grpcS, a.serviceSet.ProjectService) 798 accountpkg.RegisterAccountServiceServer(grpcS, a.serviceSet.AccountService) 799 certificatepkg.RegisterCertificateServiceServer(grpcS, a.serviceSet.CertificateService) 800 gpgkeypkg.RegisterGPGKeyServiceServer(grpcS, a.serviceSet.GpgkeyService) 801 // Register reflection service on gRPC server. 802 reflection.Register(grpcS) 803 grpc_prometheus.Register(grpcS) 804 errorsutil.CheckError(a.serviceSet.ProjectService.NormalizeProjs()) 805 return grpcS, a.serviceSet.AppResourceTreeFn 806 } 807 808 type ArgoCDServiceSet struct { 809 ClusterService *cluster.Server 810 RepoService *repository.Server 811 RepoCredsService *repocreds.Server 812 SessionService *session.Server 813 ApplicationService applicationpkg.ApplicationServiceServer 814 AppResourceTreeFn application.AppResourceTreeFn 815 ApplicationSetService applicationsetpkg.ApplicationSetServiceServer 816 ProjectService *project.Server 817 SettingsService *settings.Server 818 AccountService *account.Server 819 NotificationService notificationpkg.NotificationServiceServer 820 CertificateService *certificate.Server 821 GpgkeyService *gpgkey.Server 822 VersionService *version.Server 823 } 824 825 func newArgoCDServiceSet(a *ArgoCDServer) *ArgoCDServiceSet { 826 kubectl := kubeutil.NewKubectl() 827 clusterService := cluster.NewServer(a.db, a.enf, a.Cache, kubectl) 828 repoService := repository.NewServer(a.RepoClientset, a.db, a.enf, a.Cache, a.appLister, a.projInformer, a.Namespace, a.settingsMgr) 829 repoCredsService := repocreds.NewServer(a.RepoClientset, a.db, a.enf, a.settingsMgr) 830 var loginRateLimiter func() (io.Closer, error) 831 if maxConcurrentLoginRequestsCount > 0 { 832 loginRateLimiter = session.NewLoginRateLimiter(maxConcurrentLoginRequestsCount) 833 } 834 sessionService := session.NewServer(a.sessionMgr, a.settingsMgr, a, a.policyEnforcer, loginRateLimiter) 835 projectLock := sync.NewKeyLock() 836 applicationService, appResourceTreeFn := application.NewServer( 837 a.Namespace, 838 a.KubeClientset, 839 a.AppClientset, 840 a.appLister, 841 a.appInformer, 842 nil, 843 a.RepoClientset, 844 a.Cache, 845 kubectl, 846 a.db, 847 a.enf, 848 projectLock, 849 a.settingsMgr, 850 a.projInformer, 851 a.ApplicationNamespaces) 852 853 applicationSetService := applicationset.NewServer( 854 a.db, 855 a.KubeClientset, 856 a.enf, 857 a.AppClientset, 858 a.appsetInformer, 859 a.appsetLister, 860 a.projLister, 861 a.settingsMgr, 862 a.Namespace, 863 projectLock, 864 a.ApplicationNamespaces) 865 866 projectService := project.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.enf, projectLock, a.sessionMgr, a.policyEnforcer, a.projInformer, a.settingsMgr, a.db) 867 appsInAnyNamespaceEnabled := len(a.ArgoCDServerOpts.ApplicationNamespaces) > 0 868 settingsService := settings.NewServer(a.settingsMgr, a.RepoClientset, a, a.DisableAuth, appsInAnyNamespaceEnabled) 869 accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf) 870 871 notificationService := notification.NewServer(a.apiFactory) 872 certificateService := certificate.NewServer(a.RepoClientset, a.db, a.enf) 873 gpgkeyService := gpgkey.NewServer(a.RepoClientset, a.db, a.enf) 874 versionService := version.NewServer(a, func() (bool, error) { 875 if a.DisableAuth { 876 return true, nil 877 } 878 sett, err := a.settingsMgr.GetSettings() 879 if err != nil { 880 return false, err 881 } 882 return sett.AnonymousUserEnabled, err 883 }) 884 885 return &ArgoCDServiceSet{ 886 ClusterService: clusterService, 887 RepoService: repoService, 888 RepoCredsService: repoCredsService, 889 SessionService: sessionService, 890 ApplicationService: applicationService, 891 AppResourceTreeFn: appResourceTreeFn, 892 ApplicationSetService: applicationSetService, 893 ProjectService: projectService, 894 SettingsService: settingsService, 895 AccountService: accountService, 896 NotificationService: notificationService, 897 CertificateService: certificateService, 898 GpgkeyService: gpgkeyService, 899 VersionService: versionService, 900 } 901 } 902 903 // translateGrpcCookieHeader conditionally sets a cookie on the response. 904 func (a *ArgoCDServer) translateGrpcCookieHeader(ctx context.Context, w http.ResponseWriter, resp golang_proto.Message) error { 905 if sessionResp, ok := resp.(*sessionpkg.SessionResponse); ok { 906 token := sessionResp.Token 907 err := a.setTokenCookie(token, w) 908 if err != nil { 909 return err 910 } 911 } else if md, ok := runtime.ServerMetadataFromContext(ctx); ok { 912 renewToken := md.HeaderMD[renewTokenKey] 913 if len(renewToken) > 0 { 914 return a.setTokenCookie(renewToken[0], w) 915 } 916 } 917 918 return nil 919 } 920 921 func (a *ArgoCDServer) setTokenCookie(token string, w http.ResponseWriter) error { 922 cookiePath := fmt.Sprintf("path=/%s", strings.TrimRight(strings.TrimLeft(a.ArgoCDServerOpts.BaseHRef, "/"), "/")) 923 flags := []string{cookiePath, "SameSite=lax", "httpOnly"} 924 if !a.Insecure { 925 flags = append(flags, "Secure") 926 } 927 cookies, err := httputil.MakeCookieMetadata(common.AuthCookieName, token, flags...) 928 if err != nil { 929 return err 930 } 931 for _, cookie := range cookies { 932 w.Header().Add("Set-Cookie", cookie) 933 } 934 return nil 935 } 936 937 func withRootPath(handler http.Handler, a *ArgoCDServer) http.Handler { 938 // get rid of slashes 939 root := strings.TrimRight(strings.TrimLeft(a.RootPath, "/"), "/") 940 941 mux := http.NewServeMux() 942 mux.Handle("/"+root+"/", http.StripPrefix("/"+root, handler)) 943 944 healthz.ServeHealthCheck(mux, a.healthCheck) 945 946 return mux 947 } 948 949 func compressHandler(handler http.Handler) http.Handler { 950 compr := handlers.CompressHandler(handler) 951 return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 952 if request.Header.Get("Accept") == "text/event-stream" { 953 handler.ServeHTTP(writer, request) 954 } else { 955 compr.ServeHTTP(writer, request) 956 } 957 }) 958 } 959 960 // newHTTPServer returns the HTTP server to serve HTTP/HTTPS requests. This is implemented 961 // using grpc-gateway as a proxy to the gRPC server. 962 func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandler http.Handler, appResourceTreeFn application.AppResourceTreeFn, conn *grpc.ClientConn) *http.Server { 963 endpoint := fmt.Sprintf("localhost:%d", port) 964 mux := http.NewServeMux() 965 httpS := http.Server{ 966 Addr: endpoint, 967 Handler: &handlerSwitcher{ 968 handler: mux, 969 urlToHandler: map[string]http.Handler{ 970 "/api/badge": badge.NewHandler(a.AppClientset, a.settingsMgr, a.Namespace, a.ApplicationNamespaces), 971 common.LogoutEndpoint: logout.NewHandler(a.AppClientset, a.settingsMgr, a.sessionMgr, a.ArgoCDServerOpts.RootPath, a.ArgoCDServerOpts.BaseHRef, a.Namespace), 972 }, 973 contentTypeToHandler: map[string]http.Handler{ 974 "application/grpc-web+proto": grpcWebHandler, 975 }, 976 }, 977 } 978 979 // HTTP 1.1+JSON Server 980 // grpc-ecosystem/grpc-gateway is used to proxy HTTP requests to the corresponding gRPC call 981 // NOTE: if a marshaller option is not supplied, grpc-gateway will default to the jsonpb from 982 // golang/protobuf. Which does not support types such as time.Time. gogo/protobuf does support 983 // time.Time, but does not support custom UnmarshalJSON() and MarshalJSON() methods. Therefore 984 // we use our own Marshaler 985 gwMuxOpts := runtime.WithMarshalerOption(runtime.MIMEWildcard, new(grpc_util.JSONMarshaler)) 986 gwCookieOpts := runtime.WithForwardResponseOption(a.translateGrpcCookieHeader) 987 gwmux := runtime.NewServeMux(gwMuxOpts, gwCookieOpts) 988 989 var handler http.Handler = gwmux 990 if a.EnableGZip { 991 handler = compressHandler(handler) 992 } 993 if len(a.ContentTypes) > 0 { 994 handler = enforceContentTypes(handler, a.ContentTypes) 995 } else { 996 log.WithField(common.SecurityField, common.SecurityHigh).Warnf("Content-Type enforcement is disabled, which may make your API vulnerable to CSRF attacks") 997 } 998 mux.Handle("/api/", handler) 999 1000 terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, a.sessionMgr). 1001 WithFeatureFlagMiddleware(a.settingsMgr.GetSettings) 1002 th := util_session.WithAuthMiddleware(a.DisableAuth, a.sessionMgr, terminal) 1003 mux.Handle("/terminal", th) 1004 1005 // Proxy extension is currently an alpha feature and is disabled 1006 // by default. 1007 if a.EnableProxyExtension { 1008 // API server won't panic if extensions fail to register. In 1009 // this case an error log will be sent and no extension route 1010 // will be added in mux. 1011 registerExtensions(mux, a) 1012 } 1013 1014 mustRegisterGWHandler(versionpkg.RegisterVersionServiceHandler, ctx, gwmux, conn) 1015 mustRegisterGWHandler(clusterpkg.RegisterClusterServiceHandler, ctx, gwmux, conn) 1016 mustRegisterGWHandler(applicationpkg.RegisterApplicationServiceHandler, ctx, gwmux, conn) 1017 mustRegisterGWHandler(applicationsetpkg.RegisterApplicationSetServiceHandler, ctx, gwmux, conn) 1018 mustRegisterGWHandler(notificationpkg.RegisterNotificationServiceHandler, ctx, gwmux, conn) 1019 mustRegisterGWHandler(repositorypkg.RegisterRepositoryServiceHandler, ctx, gwmux, conn) 1020 mustRegisterGWHandler(repocredspkg.RegisterRepoCredsServiceHandler, ctx, gwmux, conn) 1021 mustRegisterGWHandler(sessionpkg.RegisterSessionServiceHandler, ctx, gwmux, conn) 1022 mustRegisterGWHandler(settingspkg.RegisterSettingsServiceHandler, ctx, gwmux, conn) 1023 mustRegisterGWHandler(projectpkg.RegisterProjectServiceHandler, ctx, gwmux, conn) 1024 mustRegisterGWHandler(accountpkg.RegisterAccountServiceHandler, ctx, gwmux, conn) 1025 mustRegisterGWHandler(certificatepkg.RegisterCertificateServiceHandler, ctx, gwmux, conn) 1026 mustRegisterGWHandler(gpgkeypkg.RegisterGPGKeyServiceHandler, ctx, gwmux, conn) 1027 1028 // Swagger UI 1029 swagger.ServeSwaggerUI(mux, assets.SwaggerJSON, "/swagger-ui", a.RootPath) 1030 healthz.ServeHealthCheck(mux, a.healthCheck) 1031 1032 // Dex reverse proxy and client app and OAuth2 login/callback 1033 a.registerDexHandlers(mux) 1034 1035 // Webhook handler for git events (Note: cache timeouts are hardcoded because API server does not write to cache and not really using them) 1036 argoDB := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset) 1037 acdWebhookHandler := webhook.NewHandler(a.Namespace, a.ArgoCDServerOpts.ApplicationNamespaces, a.AppClientset, a.settings, a.settingsMgr, repocache.NewCache(a.Cache.GetCache(), 24*time.Hour, 3*time.Minute), a.Cache, argoDB) 1038 1039 mux.HandleFunc("/api/webhook", acdWebhookHandler.Handler) 1040 1041 // Serve cli binaries directly from API server 1042 registerDownloadHandlers(mux, "/download") 1043 1044 // Serve extensions 1045 var extensionsSharedPath = "/tmp/extensions/" 1046 1047 var extensionsHandler http.Handler = http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { 1048 a.serveExtensions(extensionsSharedPath, writer) 1049 }) 1050 if a.ArgoCDServerOpts.EnableGZip { 1051 extensionsHandler = compressHandler(extensionsHandler) 1052 } 1053 mux.Handle("/extensions.js", extensionsHandler) 1054 1055 // Serve UI static assets 1056 var assetsHandler http.Handler = http.HandlerFunc(a.newStaticAssetsHandler()) 1057 if a.ArgoCDServerOpts.EnableGZip { 1058 assetsHandler = compressHandler(assetsHandler) 1059 } 1060 mux.Handle("/", assetsHandler) 1061 return &httpS 1062 } 1063 1064 func enforceContentTypes(handler http.Handler, types []string) http.Handler { 1065 allowedTypes := map[string]bool{} 1066 for _, t := range types { 1067 allowedTypes[strings.ToLower(t)] = true 1068 } 1069 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1070 if r.Method == http.MethodGet || allowedTypes[strings.ToLower(r.Header.Get("Content-Type"))] { 1071 handler.ServeHTTP(w, r) 1072 } else { 1073 http.Error(w, "Invalid content type", http.StatusUnsupportedMediaType) 1074 } 1075 }) 1076 } 1077 1078 // registerExtensions will try to register all configured extensions 1079 // in the given mux. If any error is returned while registering 1080 // extensions handlers, no route will be added in the given mux. 1081 func registerExtensions(mux *http.ServeMux, a *ArgoCDServer) { 1082 a.log.Info("Registering extensions...") 1083 extHandler := http.HandlerFunc(a.extensionManager.CallExtension()) 1084 authMiddleware := a.sessionMgr.AuthMiddlewareFunc(a.DisableAuth) 1085 // auth middleware ensures that requests to all extensions are authenticated first 1086 mux.Handle(fmt.Sprintf("%s/", extension.URLPrefix), authMiddleware(extHandler)) 1087 1088 err := a.extensionManager.RegisterExtensions() 1089 if err != nil { 1090 a.log.Errorf("Error registering extensions: %s", err) 1091 } 1092 } 1093 1094 var extensionsPattern = regexp.MustCompile(`^extension(.*)\.js$`) 1095 1096 func (a *ArgoCDServer) serveExtensions(extensionsSharedPath string, w http.ResponseWriter) { 1097 w.Header().Set("Content-Type", "application/javascript") 1098 1099 err := filepath.Walk(extensionsSharedPath, func(filePath string, info os.FileInfo, err error) error { 1100 if err != nil { 1101 return fmt.Errorf("failed to iterate files in '%s': %w", extensionsSharedPath, err) 1102 } 1103 if !files.IsSymlink(info) && !info.IsDir() && extensionsPattern.MatchString(info.Name()) { 1104 processFile := func() error { 1105 if _, err = w.Write([]byte(fmt.Sprintf("// source: %s/%s \n", filePath, info.Name()))); err != nil { 1106 return fmt.Errorf("failed to write to response: %w", err) 1107 } 1108 1109 f, err := os.Open(filePath) 1110 if err != nil { 1111 return fmt.Errorf("failed to open file '%s': %w", filePath, err) 1112 } 1113 defer io.Close(f) 1114 1115 if _, err := goio.Copy(w, f); err != nil { 1116 return fmt.Errorf("failed to copy file '%s': %w", filePath, err) 1117 } 1118 1119 return nil 1120 } 1121 1122 if processFile() != nil { 1123 return fmt.Errorf("failed to serve extension file '%s': %w", filePath, processFile()) 1124 } 1125 } 1126 return nil 1127 }) 1128 1129 if err != nil && !errors.Is(err, fs.ErrNotExist) { 1130 log.Errorf("Failed to walk extensions directory: %v", err) 1131 http.Error(w, "Internal error", http.StatusInternalServerError) 1132 return 1133 } 1134 } 1135 1136 // registerDexHandlers will register dex HTTP handlers, creating the OAuth client app 1137 func (a *ArgoCDServer) registerDexHandlers(mux *http.ServeMux) { 1138 if !a.settings.IsSSOConfigured() { 1139 return 1140 } 1141 // Run dex OpenID Connect Identity Provider behind a reverse proxy (served at /api/dex) 1142 var err error 1143 mux.HandleFunc(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy(a.DexServerAddr, a.BaseHRef, a.DexTLSConfig)) 1144 a.ssoClientApp, err = oidc.NewClientApp(a.settings, a.DexServerAddr, a.DexTLSConfig, a.BaseHRef, cacheutil.NewRedisCache(a.RedisClient, a.settings.UserInfoCacheExpiration(), cacheutil.RedisCompressionNone)) 1145 errorsutil.CheckError(err) 1146 mux.HandleFunc(common.LoginEndpoint, a.ssoClientApp.HandleLogin) 1147 mux.HandleFunc(common.CallbackEndpoint, a.ssoClientApp.HandleCallback) 1148 } 1149 1150 // newRedirectServer returns an HTTP server which does a 307 redirect to the HTTPS server 1151 func newRedirectServer(port int, rootPath string) *http.Server { 1152 addr := fmt.Sprintf("localhost:%d/%s", port, strings.TrimRight(strings.TrimLeft(rootPath, "/"), "/")) 1153 return &http.Server{ 1154 Addr: addr, 1155 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 1156 target := "https://" + req.Host 1157 if rootPath != "" { 1158 target += "/" + strings.TrimRight(strings.TrimLeft(rootPath, "/"), "/") 1159 } 1160 target += req.URL.Path 1161 if len(req.URL.RawQuery) > 0 { 1162 target += "?" + req.URL.RawQuery 1163 } 1164 http.Redirect(w, req, target, http.StatusTemporaryRedirect) 1165 }), 1166 } 1167 } 1168 1169 // registerDownloadHandlers registers HTTP handlers to support downloads directly from the API server 1170 // (e.g. argocd CLI) 1171 func registerDownloadHandlers(mux *http.ServeMux, base string) { 1172 linuxPath, err := exec.LookPath("argocd") 1173 if err != nil { 1174 log.Warnf("argocd not in PATH") 1175 } else { 1176 mux.HandleFunc(base+"/argocd-linux-"+go_runtime.GOARCH, func(w http.ResponseWriter, r *http.Request) { 1177 http.ServeFile(w, r, linuxPath) 1178 }) 1179 } 1180 } 1181 1182 func (s *ArgoCDServer) getIndexData() ([]byte, error) { 1183 s.indexDataInit.Do(func() { 1184 data, err := ui.Embedded.ReadFile("dist/app/index.html") 1185 if err != nil { 1186 s.indexDataErr = err 1187 return 1188 } 1189 if s.BaseHRef == "/" || s.BaseHRef == "" { 1190 s.indexData = data 1191 } else { 1192 s.indexData = []byte(replaceBaseHRef(string(data), fmt.Sprintf(`<base href="/%s/">`, strings.Trim(s.BaseHRef, "/")))) 1193 } 1194 }) 1195 1196 return s.indexData, s.indexDataErr 1197 } 1198 1199 func (server *ArgoCDServer) uiAssetExists(filename string) bool { 1200 f, err := server.staticAssets.Open(strings.Trim(filename, "/")) 1201 if err != nil { 1202 return false 1203 } 1204 defer io.Close(f) 1205 stat, err := f.Stat() 1206 if err != nil { 1207 return false 1208 } 1209 return !stat.IsDir() 1210 } 1211 1212 // newStaticAssetsHandler returns an HTTP handler to serve UI static assets 1213 func (server *ArgoCDServer) newStaticAssetsHandler() func(http.ResponseWriter, *http.Request) { 1214 return func(w http.ResponseWriter, r *http.Request) { 1215 acceptHTML := false 1216 for _, acceptType := range strings.Split(r.Header.Get("Accept"), ",") { 1217 if acceptType == "text/html" || acceptType == "html" { 1218 acceptHTML = true 1219 break 1220 } 1221 } 1222 1223 fileRequest := r.URL.Path != "/index.html" && server.uiAssetExists(r.URL.Path) 1224 1225 // Set X-Frame-Options according to configuration 1226 if server.XFrameOptions != "" { 1227 w.Header().Set("X-Frame-Options", server.XFrameOptions) 1228 } 1229 // Set Content-Security-Policy according to configuration 1230 if server.ContentSecurityPolicy != "" { 1231 w.Header().Set("Content-Security-Policy", server.ContentSecurityPolicy) 1232 } 1233 w.Header().Set("X-XSS-Protection", "1") 1234 1235 // serve index.html for non file requests to support HTML5 History API 1236 if acceptHTML && !fileRequest && (r.Method == "GET" || r.Method == "HEAD") { 1237 for k, v := range noCacheHeaders { 1238 w.Header().Set(k, v) 1239 } 1240 data, err := server.getIndexData() 1241 if err != nil { 1242 http.Error(w, err.Error(), http.StatusInternalServerError) 1243 return 1244 } 1245 1246 modTime, err := time.Parse(common.GetVersion().BuildDate, time.RFC3339) 1247 if err != nil { 1248 modTime = time.Now() 1249 } 1250 http.ServeContent(w, r, "index.html", modTime, io.NewByteReadSeeker(data)) 1251 } else { 1252 if isMainJsBundle(r.URL) { 1253 cacheControl := "public, max-age=31536000, immutable" 1254 if !fileRequest { 1255 cacheControl = "no-cache" 1256 } 1257 w.Header().Set("Cache-Control", cacheControl) 1258 } 1259 http.FileServer(server.staticAssets).ServeHTTP(w, r) 1260 } 1261 } 1262 } 1263 1264 var mainJsBundleRegex = regexp.MustCompile(`^main\.[0-9a-f]{20}\.js$`) 1265 1266 func isMainJsBundle(url *url.URL) bool { 1267 filename := path.Base(url.Path) 1268 return mainJsBundleRegex.Match([]byte(filename)) 1269 } 1270 1271 type registerFunc func(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error 1272 1273 // mustRegisterGWHandler is a convenience function to register a gateway handler 1274 func mustRegisterGWHandler(register registerFunc, ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) { 1275 err := register(ctx, mux, conn) 1276 if err != nil { 1277 panic(err) 1278 } 1279 } 1280 1281 func replaceBaseHRef(data string, replaceWith string) string { 1282 return baseHRefRegex.ReplaceAllString(data, replaceWith) 1283 } 1284 1285 // Authenticate checks for the presence of a valid token when accessing server-side resources. 1286 func (a *ArgoCDServer) Authenticate(ctx context.Context) (context.Context, error) { 1287 if a.DisableAuth { 1288 return ctx, nil 1289 } 1290 claims, newToken, claimsErr := a.getClaims(ctx) 1291 if claims != nil { 1292 // Add claims to the context to inspect for RBAC 1293 // nolint:staticcheck 1294 ctx = context.WithValue(ctx, "claims", claims) 1295 if newToken != "" { 1296 // Session tokens that are expiring soon should be regenerated if user stays active. 1297 // The renewed token is stored in outgoing ServerMetadata. Metadata is available to grpc-gateway 1298 // response forwarder that will translate it into Set-Cookie header. 1299 if err := grpc.SendHeader(ctx, metadata.New(map[string]string{renewTokenKey: newToken})); err != nil { 1300 log.Warnf("Failed to set %s header", renewTokenKey) 1301 } 1302 } 1303 } 1304 if claimsErr != nil { 1305 // nolint:staticcheck 1306 ctx = context.WithValue(ctx, util_session.AuthErrorCtxKey, claimsErr) 1307 } 1308 1309 if claimsErr != nil { 1310 argoCDSettings, err := a.settingsMgr.GetSettings() 1311 if err != nil { 1312 return ctx, status.Errorf(codes.Internal, "unable to load settings: %v", err) 1313 } 1314 if !argoCDSettings.AnonymousUserEnabled { 1315 return ctx, claimsErr 1316 } else { 1317 // nolint:staticcheck 1318 ctx = context.WithValue(ctx, "claims", "") 1319 } 1320 } 1321 1322 return ctx, nil 1323 } 1324 1325 func (a *ArgoCDServer) getClaims(ctx context.Context) (jwt.Claims, string, error) { 1326 md, ok := metadata.FromIncomingContext(ctx) 1327 if !ok { 1328 return nil, "", ErrNoSession 1329 } 1330 tokenString := getToken(md) 1331 if tokenString == "" { 1332 return nil, "", ErrNoSession 1333 } 1334 claims, newToken, err := a.sessionMgr.VerifyToken(tokenString) 1335 if err != nil { 1336 return claims, "", status.Errorf(codes.Unauthenticated, "invalid session: %v", err) 1337 } 1338 1339 // Some SSO implementations (Okta) require a call to 1340 // the OIDC user info path to get attributes like groups 1341 // we assume that everywhere in argocd jwt.MapClaims is used as type for interface jwt.Claims 1342 // otherwise this would cause a panic 1343 var groupClaims jwt.MapClaims 1344 if groupClaims, ok = claims.(jwt.MapClaims); !ok { 1345 if tmpClaims, ok := claims.(*jwt.MapClaims); ok { 1346 groupClaims = *tmpClaims 1347 } 1348 } 1349 iss := jwtutil.StringField(groupClaims, "iss") 1350 if iss != util_session.SessionManagerClaimsIssuer && a.settings.UserInfoGroupsEnabled() && a.settings.UserInfoPath() != "" { 1351 userInfo, unauthorized, err := a.ssoClientApp.GetUserInfo(groupClaims, a.settings.IssuerURL(), a.settings.UserInfoPath()) 1352 if unauthorized { 1353 log.Errorf("error while quering userinfo endpoint: %v", err) 1354 return claims, "", status.Errorf(codes.Unauthenticated, "invalid session") 1355 } 1356 if err != nil { 1357 log.Errorf("error fetching user info endpoint: %v", err) 1358 return claims, "", status.Errorf(codes.Internal, "invalid userinfo response") 1359 } 1360 if groupClaims["sub"] != userInfo["sub"] { 1361 return claims, "", status.Error(codes.Unknown, "subject of claims from user info endpoint didn't match subject of idToken, see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo") 1362 } 1363 groupClaims["groups"] = userInfo["groups"] 1364 } 1365 1366 return groupClaims, newToken, nil 1367 } 1368 1369 // getToken extracts the token from gRPC metadata or cookie headers 1370 func getToken(md metadata.MD) string { 1371 // check the "token" metadata 1372 { 1373 tokens, ok := md[apiclient.MetaDataTokenKey] 1374 if ok && len(tokens) > 0 { 1375 return tokens[0] 1376 } 1377 } 1378 1379 // looks for the HTTP header `Authorization: Bearer ...` 1380 // argocd prefers bearer token over cookie 1381 for _, t := range md["authorization"] { 1382 token := strings.TrimPrefix(t, "Bearer ") 1383 if strings.HasPrefix(t, "Bearer ") && jwtutil.IsValid(token) { 1384 return token 1385 } 1386 } 1387 1388 // check the HTTP cookie 1389 for _, t := range md["grpcgateway-cookie"] { 1390 header := http.Header{} 1391 header.Add("Cookie", t) 1392 request := http.Request{Header: header} 1393 token, err := httputil.JoinCookies(common.AuthCookieName, request.Cookies()) 1394 if err == nil && jwtutil.IsValid(token) { 1395 return token 1396 } 1397 } 1398 1399 return "" 1400 } 1401 1402 type handlerSwitcher struct { 1403 handler http.Handler 1404 urlToHandler map[string]http.Handler 1405 contentTypeToHandler map[string]http.Handler 1406 } 1407 1408 func (s *handlerSwitcher) ServeHTTP(w http.ResponseWriter, r *http.Request) { 1409 if urlHandler, ok := s.urlToHandler[r.URL.Path]; ok { 1410 urlHandler.ServeHTTP(w, r) 1411 } else if contentHandler, ok := s.contentTypeToHandler[r.Header.Get("content-type")]; ok { 1412 contentHandler.ServeHTTP(w, r) 1413 } else { 1414 s.handler.ServeHTTP(w, r) 1415 } 1416 } 1417 1418 // Workaround for https://github.com/golang/go/issues/21955 to support escaped URLs in URL path. 1419 type bug21955Workaround struct { 1420 handler http.Handler 1421 } 1422 1423 var pathPatters = []*regexp.Regexp{ 1424 regexp.MustCompile(`/api/v1/clusters/[^/]+`), 1425 regexp.MustCompile(`/api/v1/repositories/[^/]+`), 1426 regexp.MustCompile(`/api/v1/repocreds/[^/]+`), 1427 regexp.MustCompile(`/api/v1/repositories/[^/]+/apps`), 1428 regexp.MustCompile(`/api/v1/repositories/[^/]+/apps/[^/]+`), 1429 regexp.MustCompile(`/settings/clusters/[^/]+`), 1430 } 1431 1432 func (bf *bug21955Workaround) ServeHTTP(w http.ResponseWriter, r *http.Request) { 1433 for _, pattern := range pathPatters { 1434 if pattern.MatchString(r.URL.RawPath) { 1435 r.URL.Path = r.URL.RawPath 1436 break 1437 } 1438 } 1439 bf.handler.ServeHTTP(w, r) 1440 } 1441 1442 func bug21955WorkaroundInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 1443 if rq, ok := req.(*repositorypkg.RepoQuery); ok { 1444 repo, err := url.QueryUnescape(rq.Repo) 1445 if err != nil { 1446 return nil, err 1447 } 1448 rq.Repo = repo 1449 } else if rk, ok := req.(*repositorypkg.RepoAppsQuery); ok { 1450 repo, err := url.QueryUnescape(rk.Repo) 1451 if err != nil { 1452 return nil, err 1453 } 1454 rk.Repo = repo 1455 } else if rdq, ok := req.(*repositorypkg.RepoAppDetailsQuery); ok { 1456 repo, err := url.QueryUnescape(rdq.Source.RepoURL) 1457 if err != nil { 1458 return nil, err 1459 } 1460 rdq.Source.RepoURL = repo 1461 } else if ru, ok := req.(*repositorypkg.RepoUpdateRequest); ok { 1462 repo, err := url.QueryUnescape(ru.Repo.Repo) 1463 if err != nil { 1464 return nil, err 1465 } 1466 ru.Repo.Repo = repo 1467 } else if rk, ok := req.(*repocredspkg.RepoCredsQuery); ok { 1468 pattern, err := url.QueryUnescape(rk.Url) 1469 if err != nil { 1470 return nil, err 1471 } 1472 rk.Url = pattern 1473 } else if rk, ok := req.(*repocredspkg.RepoCredsDeleteRequest); ok { 1474 pattern, err := url.QueryUnescape(rk.Url) 1475 if err != nil { 1476 return nil, err 1477 } 1478 rk.Url = pattern 1479 } else if cq, ok := req.(*clusterpkg.ClusterQuery); ok { 1480 if cq.Id != nil { 1481 val, err := url.QueryUnescape(cq.Id.Value) 1482 if err != nil { 1483 return nil, err 1484 } 1485 cq.Id.Value = val 1486 } 1487 } else if cu, ok := req.(*clusterpkg.ClusterUpdateRequest); ok { 1488 if cu.Id != nil { 1489 val, err := url.QueryUnescape(cu.Id.Value) 1490 if err != nil { 1491 return nil, err 1492 } 1493 cu.Id.Value = val 1494 } 1495 } 1496 return handler(ctx, req) 1497 } 1498 1499 // allowedNamespacesAsString returns a string containing comma-separated list 1500 // of allowed application namespaces 1501 func (a *ArgoCDServer) allowedApplicationNamespacesAsString() string { 1502 ns := a.Namespace 1503 if len(a.ArgoCDServerOpts.ApplicationNamespaces) > 0 { 1504 ns += ", " 1505 ns += strings.Join(a.ArgoCDServerOpts.ApplicationNamespaces, ", ") 1506 } 1507 return ns 1508 }