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