github.com/argoproj/argo-cd@v1.8.7/server/server.go (about) 1 package server 2 3 import ( 4 "context" 5 "crypto/tls" 6 "fmt" 7 "io/ioutil" 8 "math" 9 "net" 10 "net/http" 11 "net/url" 12 "os" 13 "os/exec" 14 "path" 15 "regexp" 16 "strings" 17 "time" 18 19 // nolint:staticcheck 20 golang_proto "github.com/golang/protobuf/proto" 21 22 "github.com/argoproj/pkg/jwt/zjwt" 23 "github.com/argoproj/pkg/sync" 24 "github.com/dgrijalva/jwt-go/v4" 25 "github.com/go-redis/redis/v8" 26 "github.com/gorilla/handlers" 27 grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" 28 grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" 29 grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" 30 grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" 31 "github.com/grpc-ecosystem/grpc-gateway/runtime" 32 "github.com/improbable-eng/grpc-web/go/grpcweb" 33 log "github.com/sirupsen/logrus" 34 "github.com/soheilhy/cmux" 35 netCtx "golang.org/x/net/context" 36 "google.golang.org/grpc" 37 "google.golang.org/grpc/codes" 38 "google.golang.org/grpc/credentials" 39 "google.golang.org/grpc/metadata" 40 "google.golang.org/grpc/reflection" 41 "google.golang.org/grpc/status" 42 "gopkg.in/yaml.v2" 43 v1 "k8s.io/api/core/v1" 44 apierrors "k8s.io/apimachinery/pkg/api/errors" 45 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 46 "k8s.io/apimachinery/pkg/util/wait" 47 "k8s.io/client-go/kubernetes" 48 "k8s.io/client-go/tools/cache" 49 50 "github.com/argoproj/argo-cd/common" 51 "github.com/argoproj/argo-cd/pkg/apiclient" 52 accountpkg "github.com/argoproj/argo-cd/pkg/apiclient/account" 53 applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application" 54 certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate" 55 clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster" 56 gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey" 57 projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project" 58 repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds" 59 repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository" 60 sessionpkg "github.com/argoproj/argo-cd/pkg/apiclient/session" 61 settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings" 62 versionpkg "github.com/argoproj/argo-cd/pkg/apiclient/version" 63 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" 64 appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned" 65 appinformer "github.com/argoproj/argo-cd/pkg/client/informers/externalversions" 66 applisters "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1" 67 repoapiclient "github.com/argoproj/argo-cd/reposerver/apiclient" 68 repocache "github.com/argoproj/argo-cd/reposerver/cache" 69 "github.com/argoproj/argo-cd/server/account" 70 "github.com/argoproj/argo-cd/server/application" 71 "github.com/argoproj/argo-cd/server/badge" 72 servercache "github.com/argoproj/argo-cd/server/cache" 73 "github.com/argoproj/argo-cd/server/certificate" 74 "github.com/argoproj/argo-cd/server/cluster" 75 "github.com/argoproj/argo-cd/server/gpgkey" 76 "github.com/argoproj/argo-cd/server/logout" 77 "github.com/argoproj/argo-cd/server/metrics" 78 "github.com/argoproj/argo-cd/server/project" 79 "github.com/argoproj/argo-cd/server/rbacpolicy" 80 "github.com/argoproj/argo-cd/server/repocreds" 81 "github.com/argoproj/argo-cd/server/repository" 82 "github.com/argoproj/argo-cd/server/session" 83 "github.com/argoproj/argo-cd/server/settings" 84 "github.com/argoproj/argo-cd/server/version" 85 "github.com/argoproj/argo-cd/util/assets" 86 cacheutil "github.com/argoproj/argo-cd/util/cache" 87 "github.com/argoproj/argo-cd/util/db" 88 "github.com/argoproj/argo-cd/util/dex" 89 dexutil "github.com/argoproj/argo-cd/util/dex" 90 "github.com/argoproj/argo-cd/util/env" 91 "github.com/argoproj/argo-cd/util/errors" 92 grpc_util "github.com/argoproj/argo-cd/util/grpc" 93 "github.com/argoproj/argo-cd/util/healthz" 94 httputil "github.com/argoproj/argo-cd/util/http" 95 "github.com/argoproj/argo-cd/util/io" 96 kubeutil "github.com/argoproj/argo-cd/util/kube" 97 "github.com/argoproj/argo-cd/util/oidc" 98 "github.com/argoproj/argo-cd/util/rbac" 99 util_session "github.com/argoproj/argo-cd/util/session" 100 settings_util "github.com/argoproj/argo-cd/util/settings" 101 "github.com/argoproj/argo-cd/util/swagger" 102 tlsutil "github.com/argoproj/argo-cd/util/tls" 103 "github.com/argoproj/argo-cd/util/webhook" 104 ) 105 106 const maxConcurrentLoginRequestsCountEnv = "ARGOCD_MAX_CONCURRENT_LOGIN_REQUESTS_COUNT" 107 const replicasCountEnv = "ARGOCD_API_SERVER_REPLICAS" 108 109 // ErrNoSession indicates no auth token was supplied as part of a request 110 var ErrNoSession = status.Errorf(codes.Unauthenticated, "no session information") 111 112 var noCacheHeaders = map[string]string{ 113 "Expires": time.Unix(0, 0).Format(time.RFC1123), 114 "Cache-Control": "no-cache, private, max-age=0", 115 "Pragma": "no-cache", 116 "X-Accel-Expires": "0", 117 } 118 119 var backoff = wait.Backoff{ 120 Steps: 5, 121 Duration: 500 * time.Millisecond, 122 Factor: 1.0, 123 Jitter: 0.1, 124 } 125 126 var ( 127 clientConstraint = fmt.Sprintf(">= %s", common.MinClientVersion) 128 baseHRefRegex = regexp.MustCompile(`<base href="(.*)">`) 129 // limits number of concurrent login requests to prevent password brute forcing. If set to 0 then no limit is enforced. 130 maxConcurrentLoginRequestsCount = 50 131 replicasCount = 1 132 enableGRPCTimeHistogram = true 133 ) 134 135 func init() { 136 maxConcurrentLoginRequestsCount = env.ParseNumFromEnv(maxConcurrentLoginRequestsCountEnv, maxConcurrentLoginRequestsCount, 0, math.MaxInt32) 137 replicasCount = env.ParseNumFromEnv(replicasCountEnv, replicasCount, 0, math.MaxInt32) 138 if replicasCount > 0 { 139 maxConcurrentLoginRequestsCount = maxConcurrentLoginRequestsCount / replicasCount 140 } 141 enableGRPCTimeHistogram = os.Getenv(common.EnvEnableGRPCTimeHistogramEnv) == "true" 142 } 143 144 // ArgoCDServer is the API server for Argo CD 145 type ArgoCDServer struct { 146 ArgoCDServerOpts 147 148 ssoClientApp *oidc.ClientApp 149 settings *settings_util.ArgoCDSettings 150 log *log.Entry 151 sessionMgr *util_session.SessionManager 152 settingsMgr *settings_util.SettingsManager 153 enf *rbac.Enforcer 154 projInformer cache.SharedIndexInformer 155 policyEnforcer *rbacpolicy.RBACPolicyEnforcer 156 appInformer cache.SharedIndexInformer 157 appLister applisters.ApplicationNamespaceLister 158 159 // stopCh is the channel which when closed, will shutdown the Argo CD server 160 stopCh chan struct{} 161 } 162 163 type ArgoCDServerOpts struct { 164 DisableAuth bool 165 EnableGZip bool 166 Insecure bool 167 ListenPort int 168 MetricsPort int 169 Namespace string 170 DexServerAddr string 171 StaticAssetsDir string 172 BaseHRef string 173 RootPath string 174 KubeClientset kubernetes.Interface 175 AppClientset appclientset.Interface 176 RepoClientset repoapiclient.Clientset 177 Cache *servercache.Cache 178 RedisClient *redis.Client 179 TLSConfigCustomizer tlsutil.ConfigCustomizer 180 XFrameOptions string 181 } 182 183 // initializeDefaultProject creates the default project if it does not already exist 184 func initializeDefaultProject(opts ArgoCDServerOpts) error { 185 defaultProj := &v1alpha1.AppProject{ 186 ObjectMeta: metav1.ObjectMeta{Name: common.DefaultAppProjectName, Namespace: opts.Namespace}, 187 Spec: v1alpha1.AppProjectSpec{ 188 SourceRepos: []string{"*"}, 189 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 190 ClusterResourceWhitelist: []metav1.GroupKind{{Group: "*", Kind: "*"}}, 191 }, 192 } 193 194 _, err := opts.AppClientset.ArgoprojV1alpha1().AppProjects(opts.Namespace).Get(context.Background(), defaultProj.Name, metav1.GetOptions{}) 195 if apierrors.IsNotFound(err) { 196 _, err = opts.AppClientset.ArgoprojV1alpha1().AppProjects(opts.Namespace).Create(context.Background(), defaultProj, metav1.CreateOptions{}) 197 if apierrors.IsAlreadyExists(err) { 198 return nil 199 } 200 } 201 return err 202 } 203 204 // NewServer returns a new instance of the Argo CD API server 205 func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer { 206 settingsMgr := settings_util.NewSettingsManager(ctx, opts.KubeClientset, opts.Namespace) 207 settings, err := settingsMgr.InitializeSettings(opts.Insecure) 208 errors.CheckError(err) 209 err = initializeDefaultProject(opts) 210 errors.CheckError(err) 211 212 factory := appinformer.NewFilteredSharedInformerFactory(opts.AppClientset, 0, opts.Namespace, func(options *metav1.ListOptions) {}) 213 projInformer := factory.Argoproj().V1alpha1().AppProjects().Informer() 214 projLister := factory.Argoproj().V1alpha1().AppProjects().Lister().AppProjects(opts.Namespace) 215 216 appInformer := factory.Argoproj().V1alpha1().Applications().Informer() 217 appLister := factory.Argoproj().V1alpha1().Applications().Lister().Applications(opts.Namespace) 218 219 sessionMgr := util_session.NewSessionManager(settingsMgr, projLister, opts.DexServerAddr, opts.Cache) 220 enf := rbac.NewEnforcer(opts.KubeClientset, opts.Namespace, common.ArgoCDRBACConfigMapName, nil) 221 enf.EnableEnforce(!opts.DisableAuth) 222 err = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 223 errors.CheckError(err) 224 enf.EnableLog(os.Getenv(common.EnvVarRBACDebug) == "1") 225 226 policyEnf := rbacpolicy.NewRBACPolicyEnforcer(enf, projLister) 227 enf.SetClaimsEnforcerFunc(policyEnf.EnforceClaims) 228 229 return &ArgoCDServer{ 230 ArgoCDServerOpts: opts, 231 log: log.NewEntry(log.StandardLogger()), 232 settings: settings, 233 sessionMgr: sessionMgr, 234 settingsMgr: settingsMgr, 235 enf: enf, 236 projInformer: projInformer, 237 appInformer: appInformer, 238 appLister: appLister, 239 policyEnforcer: policyEnf, 240 } 241 } 242 243 const ( 244 // catches corrupted informer state; see https://github.com/argoproj/argo-cd/issues/4960 for more information 245 notObjectErrMsg = "object does not implement the Object interfaces" 246 ) 247 248 func (a *ArgoCDServer) healthCheck(r *http.Request) error { 249 if val, ok := r.URL.Query()["full"]; ok && len(val) > 0 && val[0] == "true" { 250 argoDB := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset) 251 _, err := argoDB.ListClusters(r.Context()) 252 if err != nil && strings.Contains(err.Error(), notObjectErrMsg) { 253 return err 254 } 255 } 256 return nil 257 } 258 259 // Run runs the API Server 260 // We use k8s.io/code-generator/cmd/go-to-protobuf to generate the .proto files from the API types. 261 // k8s.io/ go-to-protobuf uses protoc-gen-gogo, which comes from gogo/protobuf (a fork of 262 // golang/protobuf). 263 func (a *ArgoCDServer) Run(ctx context.Context, port int, metricsPort int) { 264 grpcS := a.newGRPCServer() 265 grpcWebS := grpcweb.WrapServer(grpcS) 266 var httpS *http.Server 267 var httpsS *http.Server 268 if a.useTLS() { 269 httpS = newRedirectServer(port, a.RootPath) 270 httpsS = a.newHTTPServer(ctx, port, grpcWebS) 271 } else { 272 httpS = a.newHTTPServer(ctx, port, grpcWebS) 273 } 274 if a.RootPath != "" { 275 httpS.Handler = withRootPath(httpS.Handler, a) 276 277 if httpsS != nil { 278 httpsS.Handler = withRootPath(httpsS.Handler, a) 279 } 280 } 281 httpS.Handler = &bug21955Workaround{handler: httpS.Handler} 282 if httpsS != nil { 283 httpsS.Handler = &bug21955Workaround{handler: httpsS.Handler} 284 } 285 286 metricsServ := metrics.NewMetricsServer(metricsPort) 287 if a.RedisClient != nil { 288 cacheutil.CollectMetrics(a.RedisClient, metricsServ) 289 } 290 291 // Start listener 292 var conn net.Listener 293 var realErr error 294 _ = wait.ExponentialBackoff(backoff, func() (bool, error) { 295 conn, realErr = net.Listen("tcp", fmt.Sprintf(":%d", port)) 296 if realErr != nil { 297 a.log.Warnf("failed listen: %v", realErr) 298 return false, nil 299 } 300 return true, nil 301 }) 302 errors.CheckError(realErr) 303 304 // Cmux is used to support servicing gRPC and HTTP1.1+JSON on the same port 305 tcpm := cmux.New(conn) 306 var tlsm cmux.CMux 307 var grpcL net.Listener 308 var httpL net.Listener 309 var httpsL net.Listener 310 if !a.useTLS() { 311 httpL = tcpm.Match(cmux.HTTP1Fast()) 312 grpcL = tcpm.Match(cmux.HTTP2HeaderField("content-type", "application/grpc")) 313 } else { 314 // We first match on HTTP 1.1 methods. 315 httpL = tcpm.Match(cmux.HTTP1Fast()) 316 317 // If not matched, we assume that its TLS. 318 tlsl := tcpm.Match(cmux.Any()) 319 tlsConfig := tls.Config{ 320 Certificates: []tls.Certificate{*a.settings.Certificate}, 321 } 322 if a.TLSConfigCustomizer != nil { 323 a.TLSConfigCustomizer(&tlsConfig) 324 } 325 tlsl = tls.NewListener(tlsl, &tlsConfig) 326 327 // Now, we build another mux recursively to match HTTPS and gRPC. 328 tlsm = cmux.New(tlsl) 329 httpsL = tlsm.Match(cmux.HTTP1Fast()) 330 grpcL = tlsm.Match(cmux.Any()) 331 } 332 333 // Start the muxed listeners for our servers 334 log.Infof("argocd %s serving on port %d (url: %s, tls: %v, namespace: %s, sso: %v)", 335 common.GetVersion(), port, a.settings.URL, a.useTLS(), a.Namespace, a.settings.IsSSOConfigured()) 336 337 go a.projInformer.Run(ctx.Done()) 338 go a.appInformer.Run(ctx.Done()) 339 go func() { a.checkServeErr("grpcS", grpcS.Serve(grpcL)) }() 340 go func() { a.checkServeErr("httpS", httpS.Serve(httpL)) }() 341 if a.useTLS() { 342 go func() { a.checkServeErr("httpsS", httpsS.Serve(httpsL)) }() 343 go func() { a.checkServeErr("tlsm", tlsm.Serve()) }() 344 } 345 go a.watchSettings() 346 go a.rbacPolicyLoader(ctx) 347 go func() { a.checkServeErr("tcpm", tcpm.Serve()) }() 348 go func() { a.checkServeErr("metrics", metricsServ.ListenAndServe()) }() 349 if !cache.WaitForCacheSync(ctx.Done(), a.projInformer.HasSynced, a.appInformer.HasSynced) { 350 log.Fatal("Timed out waiting for project cache to sync") 351 } 352 353 a.stopCh = make(chan struct{}) 354 <-a.stopCh 355 errors.CheckError(conn.Close()) 356 } 357 358 // checkServeErr checks the error from a .Serve() call to decide if it was a graceful shutdown 359 func (a *ArgoCDServer) checkServeErr(name string, err error) { 360 if err != nil { 361 if a.stopCh == nil { 362 // a nil stopCh indicates a graceful shutdown 363 log.Infof("graceful shutdown %s: %v", name, err) 364 } else { 365 log.Fatalf("%s: %v", name, err) 366 } 367 } else { 368 log.Infof("graceful shutdown %s", name) 369 } 370 } 371 372 // Shutdown stops the Argo CD server 373 func (a *ArgoCDServer) Shutdown() { 374 log.Info("Shut down requested") 375 stopCh := a.stopCh 376 a.stopCh = nil 377 if stopCh != nil { 378 close(stopCh) 379 } 380 } 381 382 // watchSettings watches the configmap and secret for any setting updates that would warrant a 383 // restart of the API server. 384 func (a *ArgoCDServer) watchSettings() { 385 updateCh := make(chan *settings_util.ArgoCDSettings, 1) 386 a.settingsMgr.Subscribe(updateCh) 387 388 prevURL := a.settings.URL 389 prevOIDCConfig := a.settings.OIDCConfigRAW 390 prevDexCfgBytes, err := dex.GenerateDexConfigYAML(a.settings) 391 errors.CheckError(err) 392 prevGitHubSecret := a.settings.WebhookGitHubSecret 393 prevGitLabSecret := a.settings.WebhookGitLabSecret 394 prevBitbucketUUID := a.settings.WebhookBitbucketUUID 395 prevBitbucketServerSecret := a.settings.WebhookBitbucketServerSecret 396 prevGogsSecret := a.settings.WebhookGogsSecret 397 var prevCert, prevCertKey string 398 if a.settings.Certificate != nil && !a.ArgoCDServerOpts.Insecure { 399 prevCert, prevCertKey = tlsutil.EncodeX509KeyPairString(*a.settings.Certificate) 400 } 401 402 for { 403 newSettings := <-updateCh 404 a.settings = newSettings 405 newDexCfgBytes, err := dex.GenerateDexConfigYAML(a.settings) 406 errors.CheckError(err) 407 if string(newDexCfgBytes) != string(prevDexCfgBytes) { 408 log.Infof("dex config modified. restarting") 409 break 410 } 411 if prevOIDCConfig != a.settings.OIDCConfigRAW { 412 log.Infof("odic config modified. restarting") 413 break 414 } 415 if prevURL != a.settings.URL { 416 log.Infof("url modified. restarting") 417 break 418 } 419 if prevGitHubSecret != a.settings.WebhookGitHubSecret { 420 log.Infof("github secret modified. restarting") 421 break 422 } 423 if prevGitLabSecret != a.settings.WebhookGitLabSecret { 424 log.Infof("gitlab secret modified. restarting") 425 break 426 } 427 if prevBitbucketUUID != a.settings.WebhookBitbucketUUID { 428 log.Infof("bitbucket uuid modified. restarting") 429 break 430 } 431 if prevBitbucketServerSecret != a.settings.WebhookBitbucketServerSecret { 432 log.Infof("bitbucket server secret modified. restarting") 433 break 434 } 435 if prevGogsSecret != a.settings.WebhookGogsSecret { 436 log.Infof("gogs secret modified. restarting") 437 break 438 } 439 if !a.ArgoCDServerOpts.Insecure { 440 var newCert, newCertKey string 441 if a.settings.Certificate != nil { 442 newCert, newCertKey = tlsutil.EncodeX509KeyPairString(*a.settings.Certificate) 443 } 444 if newCert != prevCert || newCertKey != prevCertKey { 445 log.Infof("tls certificate modified. restarting") 446 break 447 } 448 } 449 } 450 log.Info("shutting down settings watch") 451 a.Shutdown() 452 a.settingsMgr.Unsubscribe(updateCh) 453 close(updateCh) 454 } 455 456 func (a *ArgoCDServer) rbacPolicyLoader(ctx context.Context) { 457 err := a.enf.RunPolicyLoader(ctx, func(cm *v1.ConfigMap) error { 458 var scopes []string 459 if scopesStr, ok := cm.Data[rbac.ConfigMapScopesKey]; len(scopesStr) > 0 && ok { 460 scopes = make([]string, 0) 461 err := yaml.Unmarshal([]byte(scopesStr), &scopes) 462 if err != nil { 463 return err 464 } 465 } 466 467 a.policyEnforcer.SetScopes(scopes) 468 return nil 469 }) 470 errors.CheckError(err) 471 } 472 473 func (a *ArgoCDServer) useTLS() bool { 474 if a.Insecure || a.settings.Certificate == nil { 475 return false 476 } 477 return true 478 } 479 480 func (a *ArgoCDServer) newGRPCServer() *grpc.Server { 481 if enableGRPCTimeHistogram { 482 grpc_prometheus.EnableHandlingTimeHistogram() 483 } 484 485 sOpts := []grpc.ServerOption{ 486 // Set the both send and receive the bytes limit to be 100MB 487 // The proper way to achieve high performance is to have pagination 488 // while we work toward that, we can have high limit first 489 grpc.MaxRecvMsgSize(apiclient.MaxGRPCMessageSize), 490 grpc.MaxSendMsgSize(apiclient.MaxGRPCMessageSize), 491 grpc.ConnectionTimeout(300 * time.Second), 492 } 493 sensitiveMethods := map[string]bool{ 494 "/cluster.ClusterService/Create": true, 495 "/cluster.ClusterService/Update": true, 496 "/session.SessionService/Create": true, 497 "/account.AccountService/UpdatePassword": true, 498 "/gpgkey.GPGKeyService/CreateGnuPGPublicKey": true, 499 "/repository.RepositoryService/Create": true, 500 "/repository.RepositoryService/Update": true, 501 "/repository.RepositoryService/CreateRepository": true, 502 "/repository.RepositoryService/UpdateRepository": true, 503 "/repository.RepositoryService/ValidateAccess": true, 504 "/repocreds.RepoCredsService/CreateRepositoryCredentials": true, 505 "/repocreds.RepoCredsService/UpdateRepositoryCredentials": true, 506 "/application.ApplicationService/PatchResource": true, 507 } 508 // NOTE: notice we do not configure the gRPC server here with TLS (e.g. grpc.Creds(creds)) 509 // This is because TLS handshaking occurs in cmux handling 510 sOpts = append(sOpts, grpc.StreamInterceptor(grpc_middleware.ChainStreamServer( 511 grpc_logrus.StreamServerInterceptor(a.log), 512 grpc_prometheus.StreamServerInterceptor, 513 grpc_auth.StreamServerInterceptor(a.Authenticate), 514 grpc_util.UserAgentStreamServerInterceptor(common.ArgoCDUserAgentName, clientConstraint), 515 grpc_util.PayloadStreamServerInterceptor(a.log, true, func(ctx netCtx.Context, fullMethodName string, servingObject interface{}) bool { 516 return !sensitiveMethods[fullMethodName] 517 }), 518 grpc_util.ErrorCodeStreamServerInterceptor(), 519 grpc_util.PanicLoggerStreamServerInterceptor(a.log), 520 ))) 521 sOpts = append(sOpts, grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( 522 bug21955WorkaroundInterceptor, 523 grpc_logrus.UnaryServerInterceptor(a.log), 524 grpc_prometheus.UnaryServerInterceptor, 525 grpc_auth.UnaryServerInterceptor(a.Authenticate), 526 grpc_util.UserAgentUnaryServerInterceptor(common.ArgoCDUserAgentName, clientConstraint), 527 grpc_util.PayloadUnaryServerInterceptor(a.log, true, func(ctx netCtx.Context, fullMethodName string, servingObject interface{}) bool { 528 return !sensitiveMethods[fullMethodName] 529 }), 530 grpc_util.ErrorCodeUnaryServerInterceptor(), 531 grpc_util.PanicLoggerUnaryServerInterceptor(a.log), 532 ))) 533 grpcS := grpc.NewServer(sOpts...) 534 db := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset) 535 kubectl := kubeutil.NewKubectl() 536 clusterService := cluster.NewServer(db, a.enf, a.Cache, kubectl) 537 repoService := repository.NewServer(a.RepoClientset, db, a.enf, a.Cache, a.settingsMgr) 538 repoCredsService := repocreds.NewServer(a.RepoClientset, db, a.enf, a.settingsMgr) 539 var loginRateLimiter func() (io.Closer, error) 540 if maxConcurrentLoginRequestsCount > 0 { 541 loginRateLimiter = session.NewLoginRateLimiter(maxConcurrentLoginRequestsCount) 542 } 543 sessionService := session.NewServer(a.sessionMgr, a, a.policyEnforcer, loginRateLimiter) 544 projectLock := sync.NewKeyLock() 545 applicationService := application.NewServer( 546 a.Namespace, 547 a.KubeClientset, 548 a.AppClientset, 549 a.appLister, 550 a.appInformer, 551 a.RepoClientset, 552 a.Cache, 553 kubectl, 554 db, 555 a.enf, 556 projectLock, 557 a.settingsMgr, 558 a.projInformer) 559 projectService := project.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.enf, projectLock, a.sessionMgr, a.policyEnforcer, a.projInformer, a.settingsMgr) 560 settingsService := settings.NewServer(a.settingsMgr, a, a.DisableAuth) 561 accountService := account.NewServer(a.sessionMgr, a.settingsMgr, a.enf) 562 certificateService := certificate.NewServer(a.RepoClientset, db, a.enf) 563 gpgkeyService := gpgkey.NewServer(a.RepoClientset, db, a.enf) 564 versionpkg.RegisterVersionServiceServer(grpcS, version.NewServer(a, func() (bool, error) { 565 if a.DisableAuth { 566 return true, nil 567 } 568 sett, err := a.settingsMgr.GetSettings() 569 if err != nil { 570 return false, err 571 } 572 return sett.AnonymousUserEnabled, err 573 })) 574 clusterpkg.RegisterClusterServiceServer(grpcS, clusterService) 575 applicationpkg.RegisterApplicationServiceServer(grpcS, applicationService) 576 repositorypkg.RegisterRepositoryServiceServer(grpcS, repoService) 577 repocredspkg.RegisterRepoCredsServiceServer(grpcS, repoCredsService) 578 sessionpkg.RegisterSessionServiceServer(grpcS, sessionService) 579 settingspkg.RegisterSettingsServiceServer(grpcS, settingsService) 580 projectpkg.RegisterProjectServiceServer(grpcS, projectService) 581 accountpkg.RegisterAccountServiceServer(grpcS, accountService) 582 certificatepkg.RegisterCertificateServiceServer(grpcS, certificateService) 583 gpgkeypkg.RegisterGPGKeyServiceServer(grpcS, gpgkeyService) 584 // Register reflection service on gRPC server. 585 reflection.Register(grpcS) 586 grpc_prometheus.Register(grpcS) 587 errors.CheckError(projectService.NormalizeProjs()) 588 return grpcS 589 } 590 591 // TranslateGrpcCookieHeader conditionally sets a cookie on the response. 592 func (a *ArgoCDServer) translateGrpcCookieHeader(ctx context.Context, w http.ResponseWriter, resp golang_proto.Message) error { 593 if sessionResp, ok := resp.(*sessionpkg.SessionResponse); ok { 594 cookiePath := fmt.Sprintf("path=/%s", strings.TrimRight(strings.TrimLeft(a.ArgoCDServerOpts.RootPath, "/"), "/")) 595 flags := []string{cookiePath, "SameSite=lax", "httpOnly"} 596 if !a.Insecure { 597 flags = append(flags, "Secure") 598 } 599 token := sessionResp.Token 600 if token != "" { 601 var err error 602 token, err = zjwt.ZJWT(token) 603 if err != nil { 604 return err 605 } 606 } 607 cookie, err := httputil.MakeCookieMetadata(common.AuthCookieName, token, flags...) 608 if err != nil { 609 return err 610 } 611 w.Header().Set("Set-Cookie", cookie) 612 } 613 return nil 614 } 615 616 func withRootPath(handler http.Handler, a *ArgoCDServer) http.Handler { 617 // get rid of slashes 618 root := strings.TrimRight(strings.TrimLeft(a.RootPath, "/"), "/") 619 620 mux := http.NewServeMux() 621 mux.Handle("/"+root+"/", http.StripPrefix("/"+root, handler)) 622 623 healthz.ServeHealthCheck(mux, a.healthCheck) 624 625 return mux 626 } 627 628 func compressHandler(handler http.Handler) http.Handler { 629 compr := handlers.CompressHandler(handler) 630 return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 631 if request.Header.Get("Accept") == "text/event-stream" { 632 handler.ServeHTTP(writer, request) 633 } else { 634 compr.ServeHTTP(writer, request) 635 } 636 }) 637 } 638 639 // newHTTPServer returns the HTTP server to serve HTTP/HTTPS requests. This is implemented 640 // using grpc-gateway as a proxy to the gRPC server. 641 func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandler http.Handler) *http.Server { 642 endpoint := fmt.Sprintf("localhost:%d", port) 643 mux := http.NewServeMux() 644 httpS := http.Server{ 645 Addr: endpoint, 646 Handler: &handlerSwitcher{ 647 handler: mux, 648 urlToHandler: map[string]http.Handler{ 649 "/api/badge": badge.NewHandler(a.AppClientset, a.settingsMgr, a.Namespace), 650 common.LogoutEndpoint: logout.NewHandler(a.AppClientset, a.settingsMgr, a.sessionMgr, a.ArgoCDServerOpts.RootPath, a.Namespace), 651 }, 652 contentTypeToHandler: map[string]http.Handler{ 653 "application/grpc-web+proto": grpcWebHandler, 654 }, 655 }, 656 } 657 var dOpts []grpc.DialOption 658 dOpts = append(dOpts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(apiclient.MaxGRPCMessageSize))) 659 dOpts = append(dOpts, grpc.WithUserAgent(fmt.Sprintf("%s/%s", common.ArgoCDUserAgentName, common.GetVersion().Version))) 660 if a.useTLS() { 661 // The following sets up the dial Options for grpc-gateway to talk to gRPC server over TLS. 662 // grpc-gateway is just translating HTTP/HTTPS requests as gRPC requests over localhost, 663 // so we need to supply the same certificates to establish the connections that a normal, 664 // external gRPC client would need. 665 tlsConfig := a.settings.TLSConfig() 666 if a.TLSConfigCustomizer != nil { 667 a.TLSConfigCustomizer(tlsConfig) 668 } 669 tlsConfig.InsecureSkipVerify = true 670 dCreds := credentials.NewTLS(tlsConfig) 671 dOpts = append(dOpts, grpc.WithTransportCredentials(dCreds)) 672 } else { 673 dOpts = append(dOpts, grpc.WithInsecure()) 674 } 675 676 // HTTP 1.1+JSON Server 677 // grpc-ecosystem/grpc-gateway is used to proxy HTTP requests to the corresponding gRPC call 678 // NOTE: if a marshaller option is not supplied, grpc-gateway will default to the jsonpb from 679 // golang/protobuf. Which does not support types such as time.Time. gogo/protobuf does support 680 // time.Time, but does not support custom UnmarshalJSON() and MarshalJSON() methods. Therefore 681 // we use our own Marshaler 682 gwMuxOpts := runtime.WithMarshalerOption(runtime.MIMEWildcard, new(grpc_util.JSONMarshaler)) 683 gwCookieOpts := runtime.WithForwardResponseOption(a.translateGrpcCookieHeader) 684 gwmux := runtime.NewServeMux(gwMuxOpts, gwCookieOpts) 685 686 var handler http.Handler = gwmux 687 if a.EnableGZip { 688 handler = compressHandler(handler) 689 } 690 mux.Handle("/api/", handler) 691 692 mustRegisterGWHandler(versionpkg.RegisterVersionServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 693 mustRegisterGWHandler(clusterpkg.RegisterClusterServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 694 mustRegisterGWHandler(applicationpkg.RegisterApplicationServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 695 mustRegisterGWHandler(repositorypkg.RegisterRepositoryServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 696 mustRegisterGWHandler(repocredspkg.RegisterRepoCredsServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 697 mustRegisterGWHandler(sessionpkg.RegisterSessionServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 698 mustRegisterGWHandler(settingspkg.RegisterSettingsServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 699 mustRegisterGWHandler(projectpkg.RegisterProjectServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 700 mustRegisterGWHandler(accountpkg.RegisterAccountServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 701 mustRegisterGWHandler(certificatepkg.RegisterCertificateServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 702 mustRegisterGWHandler(gpgkeypkg.RegisterGPGKeyServiceHandlerFromEndpoint, ctx, gwmux, endpoint, dOpts) 703 704 // Swagger UI 705 swagger.ServeSwaggerUI(mux, assets.SwaggerJSON, "/swagger-ui", a.RootPath) 706 healthz.ServeHealthCheck(mux, a.healthCheck) 707 708 // Dex reverse proxy and client app and OAuth2 login/callback 709 a.registerDexHandlers(mux) 710 711 // Webhook handler for git events 712 acdWebhookHandler := webhook.NewHandler(a.Namespace, a.AppClientset, a.settings, a.settingsMgr, repocache.NewCache(a.Cache.GetCache(), 24*time.Hour)) 713 mux.HandleFunc("/api/webhook", acdWebhookHandler.Handler) 714 715 // Serve cli binaries directly from API server 716 registerDownloadHandlers(mux, "/download") 717 718 // Serve UI static assets 719 if a.StaticAssetsDir != "" { 720 mux.HandleFunc("/", a.newStaticAssetsHandler(a.StaticAssetsDir, a.BaseHRef)) 721 } 722 return &httpS 723 } 724 725 // registerDexHandlers will register dex HTTP handlers, creating the the OAuth client app 726 func (a *ArgoCDServer) registerDexHandlers(mux *http.ServeMux) { 727 if !a.settings.IsSSOConfigured() { 728 return 729 } 730 // Run dex OpenID Connect Identity Provider behind a reverse proxy (served at /api/dex) 731 var err error 732 mux.HandleFunc(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy(a.DexServerAddr, a.BaseHRef)) 733 if a.useTLS() { 734 tlsConfig := a.settings.TLSConfig() 735 tlsConfig.InsecureSkipVerify = true 736 } 737 a.ssoClientApp, err = oidc.NewClientApp(a.settings, a.Cache, a.DexServerAddr, a.BaseHRef) 738 errors.CheckError(err) 739 mux.HandleFunc(common.LoginEndpoint, a.ssoClientApp.HandleLogin) 740 mux.HandleFunc(common.CallbackEndpoint, a.ssoClientApp.HandleCallback) 741 } 742 743 // newRedirectServer returns an HTTP server which does a 307 redirect to the HTTPS server 744 func newRedirectServer(port int, rootPath string) *http.Server { 745 addr := fmt.Sprintf("localhost:%d/%s", port, strings.TrimRight(strings.TrimLeft(rootPath, "/"), "/")) 746 return &http.Server{ 747 Addr: addr, 748 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 749 target := "https://" + req.Host 750 if rootPath != "" { 751 target += "/" + strings.TrimRight(strings.TrimLeft(rootPath, "/"), "/") 752 } 753 target += req.URL.Path 754 if len(req.URL.RawQuery) > 0 { 755 target += "?" + req.URL.RawQuery 756 } 757 http.Redirect(w, req, target, http.StatusTemporaryRedirect) 758 }), 759 } 760 } 761 762 // registerDownloadHandlers registers HTTP handlers to support downloads directly from the API server 763 // (e.g. argocd CLI) 764 func registerDownloadHandlers(mux *http.ServeMux, base string) { 765 linuxPath, err := exec.LookPath("argocd") 766 if err != nil { 767 log.Warnf("argocd not in PATH") 768 } else { 769 mux.HandleFunc(base+"/argocd-linux-amd64", func(w http.ResponseWriter, r *http.Request) { 770 http.ServeFile(w, r, linuxPath) 771 }) 772 } 773 darwinPath, err := exec.LookPath("argocd-darwin-amd64") 774 if err != nil { 775 log.Warnf("argocd-darwin-amd64 not in PATH") 776 } else { 777 mux.HandleFunc(base+"/argocd-darwin-amd64", func(w http.ResponseWriter, r *http.Request) { 778 http.ServeFile(w, r, darwinPath) 779 }) 780 } 781 windowsPath, err := exec.LookPath("argocd-windows-amd64.exe") 782 if err != nil { 783 log.Warnf("argocd-windows-amd64.exe not in PATH") 784 } else { 785 mux.HandleFunc(base+"/argocd-windows-amd64.exe", func(w http.ResponseWriter, r *http.Request) { 786 http.ServeFile(w, r, windowsPath) 787 }) 788 } 789 } 790 791 func indexFilePath(srcPath string, baseHRef string) (string, error) { 792 if baseHRef == "/" { 793 return srcPath, nil 794 } 795 filePath := path.Join(os.TempDir(), fmt.Sprintf("index_%s.html", strings.Replace(strings.Trim(baseHRef, "/"), "/", "_", -1))) 796 f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) 797 if err != nil { 798 if os.IsExist(err) { 799 return filePath, nil 800 } 801 return "", err 802 } 803 defer io.Close(f) 804 805 data, err := ioutil.ReadFile(srcPath) 806 if err != nil { 807 return "", err 808 } 809 if baseHRef != "/" { 810 data = []byte(baseHRefRegex.ReplaceAllString(string(data), fmt.Sprintf(`<base href="/%s/">`, strings.Trim(baseHRef, "/")))) 811 } 812 _, err = f.Write(data) 813 if err != nil { 814 return "", err 815 } 816 817 return filePath, nil 818 } 819 820 func fileExists(filename string) bool { 821 info, err := os.Stat(filename) 822 if os.IsNotExist(err) { 823 return false 824 } 825 return !info.IsDir() 826 } 827 828 // newStaticAssetsHandler returns an HTTP handler to serve UI static assets 829 func (server *ArgoCDServer) newStaticAssetsHandler(dir string, baseHRef string) func(http.ResponseWriter, *http.Request) { 830 return func(w http.ResponseWriter, r *http.Request) { 831 acceptHTML := false 832 for _, acceptType := range strings.Split(r.Header.Get("Accept"), ",") { 833 if acceptType == "text/html" || acceptType == "html" { 834 acceptHTML = true 835 break 836 } 837 } 838 fileRequest := r.URL.Path != "/index.html" && fileExists(path.Join(dir, r.URL.Path)) 839 840 // Set X-Frame-Options according to configuration 841 if server.XFrameOptions != "" { 842 w.Header().Set("X-Frame-Options", server.XFrameOptions) 843 } 844 w.Header().Set("X-XSS-Protection", "1") 845 846 // serve index.html for non file requests to support HTML5 History API 847 if acceptHTML && !fileRequest && (r.Method == "GET" || r.Method == "HEAD") { 848 for k, v := range noCacheHeaders { 849 w.Header().Set(k, v) 850 } 851 indexHtmlPath, err := indexFilePath(path.Join(dir, "index.html"), baseHRef) 852 if err != nil { 853 http.Error(w, fmt.Sprintf("Unable to access index.html: %v", err), http.StatusInternalServerError) 854 return 855 } 856 http.ServeFile(w, r, indexHtmlPath) 857 } else { 858 http.ServeFile(w, r, path.Join(dir, r.URL.Path)) 859 } 860 } 861 } 862 863 type registerFunc func(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error 864 865 // mustRegisterGWHandler is a convenience function to register a gateway handler 866 func mustRegisterGWHandler(register registerFunc, ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) { 867 err := register(ctx, mux, endpoint, opts) 868 if err != nil { 869 panic(err) 870 } 871 } 872 873 // Authenticate checks for the presence of a valid token when accessing server-side resources. 874 func (a *ArgoCDServer) Authenticate(ctx context.Context) (context.Context, error) { 875 if a.DisableAuth { 876 return ctx, nil 877 } 878 claims, claimsErr := a.getClaims(ctx) 879 if claims != nil { 880 // Add claims to the context to inspect for RBAC 881 // nolint:staticcheck 882 ctx = context.WithValue(ctx, "claims", claims) 883 } 884 885 if claimsErr != nil { 886 argoCDSettings, err := a.settingsMgr.GetSettings() 887 if err != nil { 888 return ctx, status.Errorf(codes.Internal, "unable to load settings: %v", err) 889 } 890 if !argoCDSettings.AnonymousUserEnabled { 891 return ctx, claimsErr 892 } 893 } 894 895 return ctx, nil 896 } 897 898 func (a *ArgoCDServer) getClaims(ctx context.Context) (jwt.Claims, error) { 899 md, ok := metadata.FromIncomingContext(ctx) 900 if !ok { 901 return nil, ErrNoSession 902 } 903 tokenString := getToken(md) 904 if tokenString == "" { 905 return nil, ErrNoSession 906 } 907 claims, err := a.sessionMgr.VerifyToken(tokenString) 908 if err != nil { 909 return claims, status.Errorf(codes.Unauthenticated, "invalid session: %v", err) 910 } 911 return claims, nil 912 } 913 914 // getToken extracts the token from gRPC metadata or cookie headers 915 func getToken(md metadata.MD) string { 916 // check the "token" metadata 917 { 918 tokens, ok := md[apiclient.MetaDataTokenKey] 919 if ok && len(tokens) > 0 { 920 return tokens[0] 921 } 922 } 923 924 var tokens []string 925 926 // looks for the HTTP header `Authorization: Bearer ...` 927 for _, t := range md["authorization"] { 928 if strings.HasPrefix(t, "Bearer ") { 929 tokens = append(tokens, strings.TrimPrefix(t, "Bearer ")) 930 } 931 } 932 933 // check the HTTP cookie 934 for _, t := range md["grpcgateway-cookie"] { 935 header := http.Header{} 936 header.Add("Cookie", t) 937 request := http.Request{Header: header} 938 token, err := request.Cookie(common.AuthCookieName) 939 if err == nil { 940 tokens = append(tokens, token.Value) 941 } 942 } 943 944 for _, t := range tokens { 945 value, err := zjwt.JWT(t) 946 if err == nil { 947 return value 948 } 949 } 950 return "" 951 } 952 953 type handlerSwitcher struct { 954 handler http.Handler 955 urlToHandler map[string]http.Handler 956 contentTypeToHandler map[string]http.Handler 957 } 958 959 func (s *handlerSwitcher) ServeHTTP(w http.ResponseWriter, r *http.Request) { 960 if urlHandler, ok := s.urlToHandler[r.URL.Path]; ok { 961 urlHandler.ServeHTTP(w, r) 962 } else if contentHandler, ok := s.contentTypeToHandler[r.Header.Get("content-type")]; ok { 963 contentHandler.ServeHTTP(w, r) 964 } else { 965 s.handler.ServeHTTP(w, r) 966 } 967 } 968 969 // Workaround for https://github.com/golang/go/issues/21955 to support escaped URLs in URL path. 970 type bug21955Workaround struct { 971 handler http.Handler 972 } 973 974 var pathPatters = []*regexp.Regexp{ 975 regexp.MustCompile(`/api/v1/clusters/[^/]+`), 976 regexp.MustCompile(`/api/v1/repositories/[^/]+`), 977 regexp.MustCompile(`/api/v1/repocreds/[^/]+`), 978 regexp.MustCompile(`/api/v1/repositories/[^/]+/apps`), 979 regexp.MustCompile(`/api/v1/repositories/[^/]+/apps/[^/]+`), 980 regexp.MustCompile(`/settings/clusters/[^/]+`), 981 } 982 983 func (bf *bug21955Workaround) ServeHTTP(w http.ResponseWriter, r *http.Request) { 984 for _, pattern := range pathPatters { 985 if pattern.MatchString(r.URL.RawPath) { 986 r.URL.Path = r.URL.RawPath 987 break 988 } 989 } 990 bf.handler.ServeHTTP(w, r) 991 } 992 993 func bug21955WorkaroundInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 994 if rq, ok := req.(*repositorypkg.RepoQuery); ok { 995 repo, err := url.QueryUnescape(rq.Repo) 996 if err != nil { 997 return nil, err 998 } 999 rq.Repo = repo 1000 } else if rk, ok := req.(*repositorypkg.RepoAppsQuery); ok { 1001 repo, err := url.QueryUnescape(rk.Repo) 1002 if err != nil { 1003 return nil, err 1004 } 1005 rk.Repo = repo 1006 } else if rdq, ok := req.(*repositorypkg.RepoAppDetailsQuery); ok { 1007 repo, err := url.QueryUnescape(rdq.Source.RepoURL) 1008 if err != nil { 1009 return nil, err 1010 } 1011 rdq.Source.RepoURL = repo 1012 } else if ru, ok := req.(*repositorypkg.RepoUpdateRequest); ok { 1013 repo, err := url.QueryUnescape(ru.Repo.Repo) 1014 if err != nil { 1015 return nil, err 1016 } 1017 ru.Repo.Repo = repo 1018 } else if rk, ok := req.(*repocredspkg.RepoCredsQuery); ok { 1019 pattern, err := url.QueryUnescape(rk.Url) 1020 if err != nil { 1021 return nil, err 1022 } 1023 rk.Url = pattern 1024 } else if rk, ok := req.(*repocredspkg.RepoCredsDeleteRequest); ok { 1025 pattern, err := url.QueryUnescape(rk.Url) 1026 if err != nil { 1027 return nil, err 1028 } 1029 rk.Url = pattern 1030 } else if cq, ok := req.(*clusterpkg.ClusterQuery); ok { 1031 server, err := url.QueryUnescape(cq.Server) 1032 if err != nil { 1033 return nil, err 1034 } 1035 cq.Server = server 1036 } else if cu, ok := req.(*clusterpkg.ClusterUpdateRequest); ok { 1037 server, err := url.QueryUnescape(cu.Cluster.Server) 1038 if err != nil { 1039 return nil, err 1040 } 1041 cu.Cluster.Server = server 1042 } 1043 return handler(ctx, req) 1044 }