github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/pkg/cmd/server.go (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  
    17  package cmd
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"os"
    27  	"regexp"
    28  	"strings"
    29  
    30  	grpcerrors "github.com/freiheit-com/kuberpult/pkg/grpc"
    31  
    32  	"github.com/ProtonMail/go-crypto/openpgp"
    33  	"github.com/freiheit-com/kuberpult/services/frontend-service/pkg/interceptors"
    34  
    35  	"github.com/MicahParks/keyfunc/v2"
    36  	"github.com/freiheit-com/kuberpult/services/frontend-service/pkg/config"
    37  	"github.com/freiheit-com/kuberpult/services/frontend-service/pkg/service"
    38  
    39  	api "github.com/freiheit-com/kuberpult/pkg/api/v1"
    40  	"github.com/freiheit-com/kuberpult/pkg/auth"
    41  	"github.com/freiheit-com/kuberpult/pkg/logger"
    42  	"github.com/freiheit-com/kuberpult/pkg/setup"
    43  	"github.com/freiheit-com/kuberpult/pkg/tracing"
    44  	"github.com/freiheit-com/kuberpult/services/frontend-service/pkg/handler"
    45  	grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
    46  	"github.com/improbable-eng/grpc-web/go/grpcweb"
    47  	"github.com/kelseyhightower/envconfig"
    48  	"go.uber.org/zap"
    49  	"google.golang.org/api/compute/v1"
    50  	"google.golang.org/api/idtoken"
    51  	"google.golang.org/grpc"
    52  	"google.golang.org/grpc/codes"
    53  	"google.golang.org/grpc/credentials"
    54  	"google.golang.org/grpc/credentials/insecure"
    55  	"google.golang.org/grpc/status"
    56  	grpctrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc"
    57  	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
    58  )
    59  
    60  var c config.ServerConfig
    61  var backendServiceId string = ""
    62  
    63  func getBackendServiceId(c config.ServerConfig, ctx context.Context) string {
    64  	if c.GKEBackendServiceID == "" && c.GKEBackendServiceName == "" {
    65  		logger.FromContext(ctx).Warn("gke environment variables are not set up correctly! missing backend_service_id or backend_service_name")
    66  		return ""
    67  	}
    68  
    69  	if c.GKEBackendServiceID != "" && c.GKEBackendServiceName != "" {
    70  		logger.FromContext(ctx).Warn("gke environment variables are not set up correctly! backend_service_id and backend_service_name cannot be set simultaneously")
    71  		return ""
    72  	}
    73  
    74  	if c.GKEBackendServiceID != "" {
    75  		return c.GKEBackendServiceID
    76  	}
    77  	regex, err := regexp.Compile(c.GKEBackendServiceName)
    78  	if err != nil {
    79  		logger.FromContext(ctx).Warn("Error compiling regex for backend_service_name: %v", zap.Error(err))
    80  		return ""
    81  	}
    82  	computeService, err := compute.NewService(ctx)
    83  	if err != nil {
    84  		logger.FromContext(ctx).Warn("Failed to create Compute Service client: %v", zap.Error(err))
    85  		return ""
    86  	}
    87  	backendServices, err := computeService.BackendServices.List(c.GKEProjectNumber).Do()
    88  	if err != nil {
    89  		logger.FromContext(ctx).Warn("Failed to get backend service: %v", zap.Error(err))
    90  		return ""
    91  	}
    92  
    93  	serviceId := ""
    94  	for _, backendService := range backendServices.Items {
    95  		if regex.MatchString(backendService.Name) {
    96  			serviceId = fmt.Sprint(backendService.Id)
    97  		}
    98  	}
    99  	if serviceId == "" {
   100  		logger.FromContext(ctx).Warn("No backend services found matching:", zap.String("pattern", c.GKEBackendServiceName))
   101  	}
   102  	return serviceId
   103  }
   104  func readAllAndClose(r io.ReadCloser, maxBytes int64) {
   105  	_, _ = io.ReadAll(io.LimitReader(r, maxBytes))
   106  	_ = r.Close()
   107  }
   108  
   109  func readPgpKeyRing() (openpgp.KeyRing, error) {
   110  	if c.PgpKeyRingPath == "" {
   111  		return nil, nil
   112  	}
   113  	file, err := os.Open(c.PgpKeyRingPath)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	defer file.Close()
   118  	return openpgp.ReadArmoredKeyRing(file)
   119  }
   120  
   121  func RunServer() {
   122  	err := logger.Wrap(context.Background(), runServer)
   123  	if err != nil {
   124  		fmt.Printf("error: %v %#v", err, err)
   125  	}
   126  }
   127  
   128  func runServer(ctx context.Context) error {
   129  	err := envconfig.Process("kuberpult", &c)
   130  
   131  	if err != nil {
   132  		logger.FromContext(ctx).Error("config.parse", zap.Error(err))
   133  		return err
   134  	}
   135  	logger.FromContext(ctx).Warn(fmt.Sprintf("config: \n%v", c))
   136  	if c.GitAuthorEmail == "" {
   137  		msg := "DefaultGitAuthorEmail must not be empty"
   138  		logger.FromContext(ctx).Error(msg)
   139  		return fmt.Errorf(msg)
   140  	}
   141  	if c.GitAuthorName == "" {
   142  		msg := "DefaultGitAuthorName must not be empty"
   143  		logger.FromContext(ctx).Error(msg)
   144  		return fmt.Errorf(msg)
   145  	}
   146  
   147  	var jwks *keyfunc.JWKS = nil
   148  	if c.AzureEnableAuth {
   149  		jwks, err = auth.JWKSInitAzure(ctx)
   150  		if err != nil {
   151  			logger.FromContext(ctx).Fatal("Unable to initialize jwks for azure auth")
   152  			return err
   153  		}
   154  	}
   155  
   156  	logger.FromContext(ctx).Info("config.gke_project_number: " + c.GKEProjectNumber + "\n")
   157  	logger.FromContext(ctx).Info("config.gke_backend_service_id: " + c.GKEBackendServiceID + "\n")
   158  	logger.FromContext(ctx).Info("config.gke_backend_service_name: " + c.GKEBackendServiceName + "\n")
   159  
   160  	if c.GKEProjectNumber != "" {
   161  		backendServiceId = getBackendServiceId(c, ctx)
   162  	}
   163  
   164  	grpcServerLogger := logger.FromContext(ctx).Named("grpc_server")
   165  
   166  	grpcStreamInterceptors := []grpc.StreamServerInterceptor{
   167  		grpc_zap.StreamServerInterceptor(grpcServerLogger),
   168  	}
   169  	grpcUnaryInterceptors := []grpc.UnaryServerInterceptor{
   170  		grpc_zap.UnaryServerInterceptor(grpcServerLogger),
   171  	}
   172  
   173  	var cred credentials.TransportCredentials = insecure.NewCredentials()
   174  	if c.CdServerSecure {
   175  		systemRoots, err := x509.SystemCertPool()
   176  		if err != nil {
   177  			msg := "failed to read CA certificates"
   178  			return fmt.Errorf(msg)
   179  		}
   180  		//exhaustruct:ignore
   181  		cred = credentials.NewTLS(&tls.Config{
   182  			RootCAs: systemRoots,
   183  		})
   184  	}
   185  
   186  	grpcClientOpts := []grpc.DialOption{
   187  		grpc.WithTransportCredentials(cred),
   188  	}
   189  
   190  	if c.EnableTracing {
   191  		tracer.Start()
   192  		defer tracer.Stop()
   193  
   194  		grpcStreamInterceptors = append(grpcStreamInterceptors,
   195  			grpctrace.StreamServerInterceptor(grpctrace.WithServiceName(tracing.ServiceName("kuberpult-frontend-service"))),
   196  		)
   197  		grpcUnaryInterceptors = append(grpcUnaryInterceptors,
   198  			grpctrace.UnaryServerInterceptor(grpctrace.WithServiceName(tracing.ServiceName("kuberpult-frontend-service"))),
   199  		)
   200  
   201  		grpcClientOpts = append(grpcClientOpts,
   202  			grpc.WithStreamInterceptor(
   203  				grpctrace.StreamClientInterceptor(grpctrace.WithServiceName(tracing.ServiceName("kuberpult-frontend-service"))),
   204  			),
   205  			grpc.WithUnaryInterceptor(
   206  				grpctrace.UnaryClientInterceptor(grpctrace.WithServiceName(tracing.ServiceName("kuberpult-frontend-service"))),
   207  			),
   208  		)
   209  	}
   210  
   211  	var defaultUser = auth.User{
   212  		DexAuthContext: nil,
   213  		Email:          c.GitAuthorEmail,
   214  		Name:           c.GitAuthorName,
   215  	}
   216  
   217  	if c.AzureEnableAuth {
   218  		var AzureUnaryInterceptor = func(ctx context.Context,
   219  			req interface{},
   220  			info *grpc.UnaryServerInfo,
   221  			handler grpc.UnaryHandler) (interface{}, error) {
   222  			return interceptors.UnaryAuthInterceptor(ctx, req, info, handler, jwks, c.AzureClientId, c.AzureTenantId)
   223  		}
   224  		var AzureStreamInterceptor = func(
   225  			srv interface{},
   226  			stream grpc.ServerStream,
   227  			info *grpc.StreamServerInfo,
   228  			handler grpc.StreamHandler,
   229  		) error {
   230  			return interceptors.StreamAuthInterceptor(srv, stream, info, handler, jwks, c.AzureClientId, c.AzureTenantId)
   231  		}
   232  		grpcUnaryInterceptors = append(grpcUnaryInterceptors, AzureUnaryInterceptor)
   233  		grpcStreamInterceptors = append(grpcStreamInterceptors, AzureStreamInterceptor)
   234  	}
   235  
   236  	if c.DexEnabled {
   237  		// Registers Dex handlers.
   238  		_, err := auth.NewDexAppClient(c.DexClientId, c.DexClientSecret, c.DexBaseURL, auth.ReadScopes(c.DexScopes))
   239  		if err != nil {
   240  			logger.FromContext(ctx).Fatal("error registering dex handlers: ", zap.Error(err))
   241  		}
   242  	}
   243  
   244  	pgpKeyRing, err := readPgpKeyRing()
   245  	if err != nil {
   246  		logger.FromContext(ctx).Fatal("pgp.read.error", zap.Error(err))
   247  		return err
   248  	}
   249  	if c.AzureEnableAuth && pgpKeyRing == nil {
   250  		logger.FromContext(ctx).Fatal("azure.auth.error: pgpKeyRing is required to authenticate manifests when \"KUBERPULT_AZURE_ENABLE_AUTH\" is true")
   251  		return err
   252  	}
   253  
   254  	gsrv := grpc.NewServer(
   255  		grpc.ChainStreamInterceptor(grpcStreamInterceptors...),
   256  		grpc.ChainUnaryInterceptor(grpcUnaryInterceptors...),
   257  	)
   258  	cdCon, err := grpc.Dial(c.CdServer, grpcClientOpts...)
   259  	if err != nil {
   260  		logger.FromContext(ctx).Fatal("grpc.dial.error", zap.Error(err), zap.String("addr", c.CdServer))
   261  	}
   262  	var rolloutClient api.RolloutServiceClient = nil
   263  	if c.RolloutServer != "" {
   264  		rolloutCon, err := grpc.Dial(c.RolloutServer, grpcClientOpts...)
   265  		if err != nil {
   266  			logger.FromContext(ctx).Fatal("grpc.dial.error", zap.Error(err), zap.String("addr", c.RolloutServer))
   267  		}
   268  		rolloutClient = api.NewRolloutServiceClient(rolloutCon)
   269  	}
   270  
   271  	batchClient := &service.BatchServiceWithDefaultTimeout{
   272  		Inner:          api.NewBatchServiceClient(cdCon),
   273  		DefaultTimeout: c.BatchClientTimeout,
   274  	}
   275  
   276  	releaseTrainPrognosisClient := api.NewReleaseTrainPrognosisServiceClient(cdCon)
   277  
   278  	gproxy := &GrpcProxy{
   279  		OverviewClient:              api.NewOverviewServiceClient(cdCon),
   280  		BatchClient:                 batchClient,
   281  		RolloutServiceClient:        rolloutClient,
   282  		GitClient:                   api.NewGitServiceClient(cdCon),
   283  		EnvironmentServiceClient:    api.NewEnvironmentServiceClient(cdCon),
   284  		ReleaseTrainPrognosisClient: releaseTrainPrognosisClient,
   285  	}
   286  	api.RegisterOverviewServiceServer(gsrv, gproxy)
   287  	api.RegisterBatchServiceServer(gsrv, gproxy)
   288  	api.RegisterRolloutServiceServer(gsrv, gproxy)
   289  	api.RegisterGitServiceServer(gsrv, gproxy)
   290  	api.RegisterEnvironmentServiceServer(gsrv, gproxy)
   291  
   292  	frontendConfigService := &service.FrontendConfigServiceServer{
   293  		Config: config.FrontendConfig{
   294  			ArgoCd: &config.ArgoCdConfig{
   295  				BaseUrl:   c.ArgocdBaseUrl,
   296  				Namespace: c.ArgocdNamespace,
   297  			},
   298  			Auth: &config.AuthConfig{
   299  				AzureAuth: &config.AzureAuthConfig{
   300  					Enabled:       c.AzureEnableAuth,
   301  					ClientId:      c.AzureClientId,
   302  					TenantId:      c.AzureTenantId,
   303  					RedirectURL:   c.AzureRedirectUrl,
   304  					CloudInstance: c.AzureCloudInstance,
   305  				},
   306  			},
   307  			ManifestRepoUrl:  c.ManifestRepoUrl,
   308  			SourceRepoUrl:    c.SourceRepoUrl,
   309  			KuberpultVersion: c.Version,
   310  			Branch:           c.GitBranch,
   311  		},
   312  	}
   313  
   314  	api.RegisterFrontendConfigServiceServer(gsrv, frontendConfigService)
   315  
   316  	grpcWebServer := grpcweb.WrapServer(gsrv)
   317  	httpHandler := handler.Server{
   318  		BatchClient:                 batchClient,
   319  		RolloutClient:               rolloutClient,
   320  		VersionClient:               api.NewVersionServiceClient(cdCon),
   321  		ReleaseTrainPrognosisClient: releaseTrainPrognosisClient,
   322  		Config:                      c,
   323  		KeyRing:                     pgpKeyRing,
   324  		AzureAuth:                   c.AzureEnableAuth,
   325  	}
   326  	mux := http.NewServeMux()
   327  	restHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   328  		defer readAllAndClose(req.Body, 1024)
   329  		if c.DexEnabled {
   330  			interceptors.DexLoginInterceptor(w, req, httpHandler.Handle, c.DexClientId, c.DexClientSecret)
   331  			return
   332  		}
   333  		httpHandler.Handle(w, req)
   334  	})
   335  	for _, endpoint := range []string{
   336  		"/environments",
   337  		"/environments/",
   338  		"/environment-groups",
   339  		"/environment-groups/",
   340  		"/release",
   341  	} {
   342  		mux.Handle(endpoint, restHandler)
   343  	}
   344  
   345  	// api is only accessible via IAP for now unless explicitly disabled
   346  	restApiHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   347  		defer readAllAndClose(req.Body, 1024)
   348  		if c.ApiEnableDespiteNoAuth {
   349  			httpHandler.HandleAPI(w, req)
   350  			return
   351  		}
   352  
   353  		if !c.IapEnabled {
   354  			http.Error(w, "IAP not enabled, /api unavailable.", http.StatusUnauthorized)
   355  			return
   356  		}
   357  		interceptors.GoogleIAPInterceptor(w, req, httpHandler.HandleAPI, backendServiceId, c.GKEProjectNumber)
   358  	})
   359  	for _, endpoint := range []string{
   360  		"/api",
   361  		"/api/",
   362  	} {
   363  		mux.Handle(endpoint, restApiHandler)
   364  	}
   365  
   366  	mux.Handle("/", http.FileServer(http.Dir("build")))
   367  	// Split HTTP REST from gRPC Web requests, as suggested in the documentation:
   368  	// https://pkg.go.dev/github.com/improbable-eng/grpc-web@v0.15.0/go/grpcweb
   369  	splitGrpcHandler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
   370  		if grpcWebServer.IsGrpcWebRequest(req) {
   371  			grpcWebServer.ServeHTTP(resp, req)
   372  		} else {
   373  			/**
   374  			The htst header is a security feature that tells the browser:
   375  			"If someone requests anything on this domain via http, do not do that request, instead make the request with https"
   376  			Docs: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
   377  			Wiki: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
   378  			Parameter "preload" is not necessary as kuberpult is not on a publicly available domain.
   379  			Parameter "includeSubDomains" is not really necessary for kuberpult right now,
   380  			  but should be set anyway in case we ever have subdomains.
   381  			31536000 seconds = 1 year.
   382  			*/
   383  			resp.Header().Set("strict-Transport-Security", "max-age=31536000; includeSubDomains;")
   384  			/**
   385  			- self is generally sufficient for most sources
   386  			- fonts.googleapis.com is used for font hosting
   387  			- unsafe-inline is needed at the moment to make emotionjs work
   388  			- fonts.gstatic.con is used for font hosting
   389  			- login.microsoftonline.com is used for azure login
   390  			*/
   391  			resp.Header().Set("Content-Security-Policy", "default-src 'self'; style-src-elem 'self' fonts.googleapis.com 'unsafe-inline'; font-src fonts.gstatic.com; connect-src 'self' login.microsoftonline.com; child-src 'none'")
   392  			// We are not using referrer headers.
   393  			resp.Header().Set("Referrer-Policy", "no-referrer")
   394  			// We don't want to be displayed in frames
   395  			resp.Header().Set("X-Frame-Options", "DENY")
   396  			// Don't sniff content-type
   397  			resp.Header().Set("X-Content-Type-Options", "nosniff")
   398  			// We don't need any special browser features.
   399  			// This policy was generated using https://www.permissionspolicy.com/
   400  			// with "Disable all" for all implemented and proposed features as of may 2023.
   401  			resp.Header().Add("Permission-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=()")
   402  
   403  			if c.AzureEnableAuth {
   404  				// these are the paths and prefixes that must not have azure authentication, in order to bootstrap the html, js, etc:
   405  				var allowedPaths = []string{"/", "/release", "/health", "/manifest.json", "/favicon.png"}
   406  				var allowedPrefixes = []string{"/static/js", "/static/css", "/ui"}
   407  				if err := auth.HttpAuthMiddleWare(resp, req, jwks, c.AzureClientId, c.AzureTenantId, allowedPaths, allowedPrefixes); err != nil {
   408  					return
   409  				}
   410  			}
   411  			/**
   412  			When the user requests any path under "/ui", we always return the same index.html (because it's a single page application).
   413  			Anything else may be another valid rest request, like /health or /release.
   414  			*/
   415  			isUi := strings.HasPrefix(req.URL.Path, "/ui")
   416  			isHtml := req.URL.Path == "/" || req.URL.Path == "/index.html"
   417  			doNotCache := isUi || isHtml
   418  			if doNotCache {
   419  				resp.Header().Set("Cache-Control", "no-cache,no-store,must-revalidate,max-age=0")
   420  			} else {
   421  				resp.Header().Set("Cache-Control", "max-age=604800") // 7 days
   422  			}
   423  			if isUi {
   424  				// this is called for example for requests to /ui, /ui/home
   425  				http.ServeFile(resp, req, "build/index.html")
   426  			} else {
   427  				// this is called for example for requests to /, /index.html,css and js
   428  				mux.ServeHTTP(resp, req)
   429  			}
   430  		}
   431  	})
   432  	authHandler := &Auth{
   433  		HttpServer:  splitGrpcHandler,
   434  		DefaultUser: defaultUser,
   435  		KeyRing:     pgpKeyRing,
   436  	}
   437  	corsHandler := &setup.CORSMiddleware{
   438  		PolicyFor: func(r *http.Request) *setup.CORSPolicy {
   439  			return &setup.CORSPolicy{
   440  				MaxAge:           0,
   441  				AllowMethods:     "POST",
   442  				AllowHeaders:     "content-type,x-grpc-web,authorization",
   443  				AllowOrigin:      c.AllowedOrigins,
   444  				AllowCredentials: true,
   445  			}
   446  		},
   447  		NextHandler: authHandler,
   448  	}
   449  
   450  	setup.Run(ctx, setup.ServerConfig{
   451  		GRPC:       nil,
   452  		Background: nil,
   453  		Shutdown:   nil,
   454  		HTTP: []setup.HTTPConfig{
   455  			{
   456  				BasicAuth: nil,
   457  				Shutdown:  nil,
   458  				Port:      "8081",
   459  				Register: func(mux *http.ServeMux) {
   460  					mux.Handle("/", corsHandler)
   461  				},
   462  			},
   463  		},
   464  	})
   465  	return nil
   466  }
   467  
   468  type Auth struct {
   469  	HttpServer  http.Handler
   470  	DefaultUser auth.User
   471  	// KeyRing is as of now required because we do not have technical users yet. So we protect public endpoints by requiring a signature
   472  	KeyRing openpgp.KeyRing
   473  }
   474  
   475  func getRequestAuthorFromGoogleIAP(ctx context.Context, r *http.Request) *auth.User {
   476  	iapJWT := r.Header.Get("X-Goog-IAP-JWT-Assertion")
   477  	if iapJWT == "" {
   478  		// not using iap (local), default user
   479  		logger.FromContext(ctx).Info("iap.jwt header was not found or doesn't exist")
   480  		return nil
   481  	}
   482  
   483  	if backendServiceId == "" {
   484  		logger.FromContext(ctx).Warn("Failed to get backend_service_id! Author information will be lost. Make sure gke environment variables are set up correctly.")
   485  		return nil
   486  	}
   487  
   488  	aud := fmt.Sprintf("/projects/%s/global/backendServices/%s", c.GKEProjectNumber, backendServiceId)
   489  	payload, err := idtoken.Validate(ctx, iapJWT, aud)
   490  	if err != nil {
   491  		logger.FromContext(ctx).Warn("iap.idtoken.validate", zap.Error(err))
   492  		return nil
   493  	}
   494  
   495  	// here, we can use People api later to get the full name
   496  
   497  	// get the authenticated email
   498  	u := &auth.User{
   499  		Name:           "",
   500  		DexAuthContext: nil,
   501  		Email:          payload.Claims["email"].(string),
   502  	}
   503  	return u
   504  }
   505  
   506  func getRequestAuthorFromAzure(ctx context.Context, r *http.Request) (*auth.User, error) {
   507  	return auth.ReadUserFromHttpHeader(ctx, r)
   508  }
   509  
   510  func (p *Auth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   511  	err := logger.Wrap(r.Context(), func(ctx context.Context) error {
   512  		span, ctx := tracer.StartSpanFromContext(ctx, "ServeHTTP")
   513  		defer span.Finish()
   514  		var user *auth.User = nil
   515  		var err error
   516  		var source string
   517  		if c.AzureEnableAuth {
   518  			user, err = getRequestAuthorFromAzure(ctx, r)
   519  			if err != nil {
   520  				return err
   521  			}
   522  			source = "azure"
   523  		} else {
   524  			user = getRequestAuthorFromGoogleIAP(ctx, r)
   525  			source = "iap"
   526  		}
   527  		if user != nil {
   528  			span.SetTag("current-user-name", user.Name)
   529  			span.SetTag("current-user-email", user.Email)
   530  			span.SetTag("current-user-source", source)
   531  		}
   532  		combinedUser := auth.GetUserOrDefault(user, p.DefaultUser)
   533  
   534  		auth.WriteUserToHttpHeader(r, combinedUser)
   535  		ctx = auth.WriteUserToContext(ctx, combinedUser)
   536  		ctx = auth.WriteUserToGrpcContext(ctx, combinedUser)
   537  		p.HttpServer.ServeHTTP(w, r.WithContext(ctx))
   538  		return nil
   539  	})
   540  	if err != nil {
   541  		fmt.Printf("error: %v %#v", err, err)
   542  	}
   543  }
   544  
   545  // GrpcProxy passes through gRPC messages to another server.
   546  // This is needed for the UI to communicate with other services via gRPC over web.
   547  // The UI _only_ communicates via gRPC over web (+ static files), while the REST API is only intended for automated processes like build pipelines.
   548  // An alternative to the more generic methods proposed in
   549  // https://github.com/grpc/grpc-go/issues/2297
   550  type GrpcProxy struct {
   551  	OverviewClient              api.OverviewServiceClient
   552  	BatchClient                 api.BatchServiceClient
   553  	RolloutServiceClient        api.RolloutServiceClient
   554  	GitClient                   api.GitServiceClient
   555  	EnvironmentServiceClient    api.EnvironmentServiceClient
   556  	ReleaseTrainPrognosisClient api.ReleaseTrainPrognosisServiceClient
   557  }
   558  
   559  func (p *GrpcProxy) ProcessBatch(
   560  	ctx context.Context,
   561  	in *api.BatchRequest) (*api.BatchResponse, error) {
   562  	for i := range in.Actions {
   563  		batchAction := in.GetActions()[i]
   564  		switch batchAction.Action.(type) {
   565  		case *api.BatchAction_CreateRelease:
   566  			return nil, grpcerrors.PublicError(ctx, fmt.Errorf("action create-release is only supported via http in the frontend-service"))
   567  		}
   568  	}
   569  
   570  	return p.BatchClient.ProcessBatch(ctx, in)
   571  }
   572  
   573  func (p *GrpcProxy) GetOverview(
   574  	ctx context.Context,
   575  	in *api.GetOverviewRequest) (*api.GetOverviewResponse, error) {
   576  	return p.OverviewClient.GetOverview(ctx, in)
   577  }
   578  
   579  func (p *GrpcProxy) GetGitTags(
   580  	ctx context.Context,
   581  	in *api.GetGitTagsRequest) (*api.GetGitTagsResponse, error) {
   582  	return p.GitClient.GetGitTags(ctx, in)
   583  }
   584  
   585  func (p *GrpcProxy) GetProductSummary(
   586  	ctx context.Context,
   587  	in *api.GetProductSummaryRequest) (*api.GetProductSummaryResponse, error) {
   588  	return p.GitClient.GetProductSummary(ctx, in)
   589  }
   590  
   591  func (p *GrpcProxy) GetCommitInfo(
   592  	ctx context.Context,
   593  	in *api.GetCommitInfoRequest) (*api.GetCommitInfoResponse, error) {
   594  	return p.GitClient.GetCommitInfo(ctx, in)
   595  }
   596  
   597  func (p *GrpcProxy) GetEnvironmentConfig(
   598  	ctx context.Context,
   599  	in *api.GetEnvironmentConfigRequest) (*api.GetEnvironmentConfigResponse, error) {
   600  	return p.EnvironmentServiceClient.GetEnvironmentConfig(ctx, in)
   601  }
   602  
   603  func (p *GrpcProxy) StreamOverview(
   604  	in *api.GetOverviewRequest,
   605  	stream api.OverviewService_StreamOverviewServer) error {
   606  	if resp, err := p.OverviewClient.StreamOverview(stream.Context(), in); err != nil {
   607  		return err
   608  	} else {
   609  		for {
   610  			if item, err := resp.Recv(); err != nil {
   611  				return err
   612  			} else {
   613  				if err := stream.Send(item); err != nil {
   614  					return err
   615  				}
   616  			}
   617  		}
   618  	}
   619  }
   620  
   621  func (p *GrpcProxy) StreamStatus(in *api.StreamStatusRequest, stream api.RolloutService_StreamStatusServer) error {
   622  	if p.RolloutServiceClient == nil {
   623  		return status.Error(codes.Unimplemented, "rollout service not configured")
   624  	}
   625  	if resp, err := p.RolloutServiceClient.StreamStatus(stream.Context(), in); err != nil {
   626  		return err
   627  	} else {
   628  		for {
   629  			item, err := resp.Recv()
   630  			if err != nil {
   631  				return err
   632  			}
   633  			err = stream.Send(item)
   634  			if err != nil {
   635  				return err
   636  			}
   637  		}
   638  	}
   639  }
   640  
   641  func (p *GrpcProxy) GetStatus(ctx context.Context, in *api.GetStatusRequest) (*api.GetStatusResponse, error) {
   642  	if p.RolloutServiceClient == nil {
   643  		return nil, status.Error(codes.Unimplemented, "rollout service not configured")
   644  	}
   645  	return p.RolloutServiceClient.GetStatus(ctx, in)
   646  }
   647  
   648  func (p *GrpcProxy) GetReleaseTrainPrognosis(ctx context.Context, in *api.ReleaseTrainRequest) (*api.GetReleaseTrainPrognosisResponse, error) {
   649  	if p.ReleaseTrainPrognosisClient == nil {
   650  		logger.FromContext(ctx).Error("release train prognosis service received a request when it is not configured")
   651  		return nil, status.Error(codes.Internal, "release train prognosis service not configured")
   652  	}
   653  	return p.ReleaseTrainPrognosisClient.GetReleaseTrainPrognosis(ctx, in)
   654  }