...

Source file src/edge-infra.dev/pkg/edge/auth-proxy/setup.go

Documentation: edge-infra.dev/pkg/edge/auth-proxy

     1  package authproxy
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/gob"
     7  	"io"
     8  	"net/http"
     9  	"net/http/httputil"
    10  	"net/url"
    11  	"os"
    12  	"regexp"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/gin-contrib/cors"
    18  	"github.com/gin-contrib/requestid"
    19  	"github.com/gin-contrib/sessions"
    20  
    21  	"github.com/gin-gonic/gin"
    22  	"github.com/go-logr/logr"
    23  
    24  	"edge-infra.dev/pkg/edge/api/middleware"
    25  	"edge-infra.dev/pkg/edge/audit"
    26  	"edge-infra.dev/pkg/edge/auth-proxy/handlers"
    27  	"edge-infra.dev/pkg/edge/auth-proxy/interceptor"
    28  
    29  	edgestore "edge-infra.dev/pkg/edge/auth-proxy/store"
    30  	"edge-infra.dev/pkg/edge/auth-proxy/types"
    31  	logger "edge-infra.dev/pkg/lib/logging"
    32  	"edge-infra.dev/pkg/lib/runtime/manager"
    33  	"edge-infra.dev/pkg/lib/uuid"
    34  	"edge-infra.dev/pkg/x/tonic"
    35  )
    36  
    37  const (
    38  	accessControlAllowOriginHeader = "Access-Control-Allow-Origin"
    39  	credentialsHeader              = "Credentials"
    40  	authorizationHeader            = "Authorization"
    41  	setCookieHeader                = "Set-Cookie"
    42  	emergencyAccessPathRegex       = `^/ea/`
    43  )
    44  
    45  func init() {
    46  	// register other types for cookie codec
    47  	gob.Register(time.Time{})
    48  }
    49  
    50  func RunManager() {
    51  	log := logger.NewLogger().Logger.WithName("auth-proxy")
    52  
    53  	cfg, err := NewConfig(os.Args[1:])
    54  	if err != nil {
    55  		log.Error(err, "failed to parse startup configuration")
    56  		os.Exit(1)
    57  	}
    58  
    59  	db, err := cfg.connectDatabase()
    60  	if err != nil {
    61  		log.Error(err, "failed to parse startup configuration")
    62  		os.Exit(1)
    63  	}
    64  
    65  	mgr, err := manager.New(manager.Options{MetricsBindAddress: cfg.MetricsPort})
    66  	if err != nil {
    67  		log.Error(err, "failed to create a new manager for auth-proxy")
    68  		os.Exit(1)
    69  	}
    70  
    71  	auditLog := audit.New("auth-proxy")
    72  
    73  	pgstore, err := edgestore.NewStore(db, log, auditLog, []byte(cfg.SessionSecret))
    74  	if err != nil {
    75  		log.Error(err, "failed to create a new manager for auth-proxy")
    76  		os.Exit(1)
    77  	}
    78  
    79  	sameSite := http.SameSiteNoneMode
    80  	if cfg.Mode == gin.ReleaseMode {
    81  		sameSite = http.SameSiteStrictMode
    82  	}
    83  
    84  	// max size of bytea type in DB is 1 GB
    85  	if err := pgstore.MaxLength(cfg.SessionLength); err != nil {
    86  		log.Error(err, "failed to set session value maximum length")
    87  		os.Exit(1)
    88  	}
    89  
    90  	pgstore.Options(sessions.Options{
    91  		Path:     "/",
    92  		HttpOnly: true,
    93  		SameSite: sameSite,
    94  		Secure:   true,
    95  		MaxAge:   60 * 14,
    96  	})
    97  
    98  	// cleanup expired sessions every 10 minutes
    99  	quit, done := pgstore.Cleanup(10 * time.Minute)
   100  	defer pgstore.StopCleanup(quit, done)
   101  
   102  	uiProxyServer := setupServer(log, auditLog, cfg, pgstore)
   103  
   104  	if err := mgr.Add(uiProxyServer); err != nil {
   105  		log.Error(err, "failed to add ui proxy server")
   106  		os.Exit(1)
   107  	}
   108  
   109  	if err := mgr.Start(context.Background()); err != nil {
   110  		log.Error(err, "auth-proxy mgr start failed")
   111  		os.Exit(1)
   112  	}
   113  }
   114  
   115  func corsMiddleware(allowedOrigins []string) gin.HandlerFunc {
   116  	corsConfig := cors.DefaultConfig()
   117  	corsConfig.AllowAllOrigins = false
   118  	corsConfig.ExposeHeaders = append(corsConfig.AllowHeaders, accessControlAllowOriginHeader, credentialsHeader, authorizationHeader, setCookieHeader)
   119  	corsConfig.AllowOrigins = allowedOrigins
   120  	corsConfig.AllowHeaders = append(corsConfig.AllowHeaders, accessControlAllowOriginHeader, credentialsHeader, authorizationHeader, setCookieHeader)
   121  	corsConfig.AllowCredentials = true
   122  	return cors.New(corsConfig)
   123  }
   124  
   125  func setupServer(log logr.Logger, auditLog *audit.Sink, cfg *ProxyConfig, store edgestore.Store) *tonic.Server {
   126  	allowedOrigins := cfg.allowedOrigins()
   127  	log.Info("configuring server with these allowed origins", "origins", allowedOrigins)
   128  	uiProxyServer := tonic.NewWithOptions(cfg.UIProxyPort, cfg.Mode).
   129  		SetLogger(log).
   130  		SetMiddlewares(middleware.SetRequestContext(), requestid.New(), middleware.RequestLogger(log), gin.Recovery(), corsMiddleware(allowedOrigins), sessions.Sessions(edgestore.SessionIdentifier, store)).
   131  		SetRoutes(tonic.Route{
   132  			Path:     "/*proxyPath",
   133  			Action:   tonic.MethodAny,
   134  			Handlers: []gin.HandlerFunc{proxyHandler(log, auditLog, cfg)},
   135  		}).With404Route()
   136  
   137  	return uiProxyServer
   138  }
   139  
   140  func proxyHandler(log logr.Logger, auditLog *audit.Sink, cfg *ProxyConfig) gin.HandlerFunc {
   141  	return func(c *gin.Context) {
   142  		proxy(c, log, auditLog, cfg)
   143  	}
   144  }
   145  
   146  func proxy(c *gin.Context, log logr.Logger, auditLog *audit.Sink, cfg *ProxyConfig) {
   147  	session := sessions.Default(c)
   148  	apiEndpoint := getAPIEndpoint(c, log, cfg)
   149  	correlationID := uuid.New().UUID
   150  	auditLog.SetCorrelationID(correlationID)
   151  
   152  	if filterEmergencyAccessURL(apiEndpoint.Path) && !validSession(session) {
   153  		// All emergencyaccess services must be protected by a validSession.
   154  		// If the session is invalid return immediately without proxying the request.
   155  		log.Info("Abandoning emergencyaccess request due to invalid session", "url", apiEndpoint)
   156  		c.AbortWithStatus(http.StatusForbidden)
   157  		return
   158  	}
   159  
   160  	log.Info("proxying request...", "url", apiEndpoint)
   161  	handler := handlers.New(c, log, session, cfg.TokenSecret, cfg.SessionDuration, false, correlationID)
   162  	proxy := httputil.NewSingleHostReverseProxy(apiEndpoint)
   163  	proxy.Rewrite = func(r *httputil.ProxyRequest) {
   164  		r.In.Host = apiEndpoint.Host
   165  		r.Out.URL = apiEndpoint
   166  		r.Out.RequestURI = r.Out.URL.String()
   167  		r.Out.RemoteAddr = r.In.RemoteAddr
   168  		r.Out.Header.Set(audit.AuditIdentifierHeader, correlationID)
   169  		// TODO(pa250194_ncrvoyix): refactor and make cleaner
   170  		// remove nolint
   171  		if r.Out.Body != nil { //nolint
   172  			body, err := io.ReadAll(r.Out.Body)
   173  			if err != nil {
   174  				log.Error(err, "error reading proxied response")
   175  				return
   176  			}
   177  			if err := r.Out.Body.Close(); err != nil {
   178  				log.Error(err, "error closing proxy response body")
   179  				return
   180  			}
   181  			r.Out.Body = io.NopCloser((bytes.NewBuffer(body)))
   182  			intercpt := interceptor.New(c.ClientIP(), correlationID, session)
   183  			intercpt.Query("schemaIntrospection", handler.Default, `\{"__schema":\{`, true)
   184  			intercpt.Query("typeIntrospection", handler.Default, `\{"__type":\{`, true)
   185  			intercpt.Query("typenameIntrospection", handler.Default, `\{"__typename":\{`, true)
   186  			intercpt.Mutation("login", handler.Login, `\{"login":\{`, true)
   187  			intercpt.Mutation("loginWithOkta", handler.LoginWithOkta, `\{"loginWithOkta":\{`, true)
   188  			intercpt.Query("tenantsForOktaToken", handler.Default, `\{"tenantsForOktaToken":\{`, true)
   189  			intercpt.Mutation("logout", handler.Logout, `\{"logout":true\}`, true)
   190  			intercpt.Mutation("sessionRefresh", handler.SessionRefresh, `\{"sessionRefresh":`, validSession(session))
   191  			intercpt.Query("sessionUserEdgeRole", handler.SessionDefault, `\{"sessionUserEdgeRole":`, validSession(session))
   192  			intercpt.Path("sessionUserDetails", handler.SessionUserDetails, emergencyAccessPathRegex, true)
   193  			intercpt.Default(handler.SessionDefault, true)
   194  			req, _, err := intercpt.Intercept(r.Out, body, true)
   195  			if err != nil {
   196  				log.Error(err, "an error occurred intercepting request body")
   197  				return
   198  			}
   199  			r.Out = req
   200  		}
   201  	}
   202  	proxy.Transport = New(apiEndpoint)
   203  	proxy.Director = nil
   204  	proxy.ModifyResponse = func(r *http.Response) error {
   205  		// If the path is of the form /api/ea/*, we want to immediately return without doing anything.
   206  		// This is because an emergency access session is a long running connection, and reading the body
   207  		// on some emergency access API requests would block until the session is closed.
   208  		if filterEmergencyAccessURL(r.Request.URL.Path) {
   209  			return nil
   210  		}
   211  		//TODO(pa250194_ncrvoyix): only Access-Control-Allow-Origin delete header if the vals already exist
   212  		r.Header.Del(accessControlAllowOriginHeader)
   213  		body, err := io.ReadAll(r.Body)
   214  		if err != nil {
   215  			log.Error(err, "error reading proxied response")
   216  			return err
   217  		}
   218  		if err := r.Body.Close(); err != nil {
   219  			log.Error(err, "error closing proxy response body")
   220  			return err
   221  		}
   222  		intercpt := interceptor.New(c.ClientIP(), correlationID, session)
   223  		intercpt.Query("schemaIntrospection", handler.Default, `\{"__schema":\{`, true)
   224  		intercpt.Query("typeIntrospection", handler.Default, `\{"__type":\{`, true)
   225  		intercpt.Query("typenameIntrospection", handler.Default, `\{"__typename":\{`, true)
   226  		intercpt.Mutation("login", handler.Login, `\{"login":\{`, true)
   227  		intercpt.Query("tenantsForOktaToken", handler.Default, `\{"tenantsForOktaToken":\{`, true)
   228  		intercpt.Mutation("loginWithOkta", handler.LoginWithOkta, `\{"loginWithOkta":\{`, true)
   229  		intercpt.Mutation("logout", handler.Logout, `\{"logout":true\}`, true)
   230  		intercpt.Mutation("sessionRefresh", handler.SessionRefresh, `\{"sessionRefresh":`, validSession(session))
   231  		intercpt.Query("sessionUserEdgeRole", handler.SessionUserEdgeRole, `\{"sessionUserEdgeRole":`, validSession(session))
   232  		intercpt.Default(handler.SessionDefault, true)
   233  		reqHeader := r.Request.Clone(r.Request.Context())
   234  		hdr, responseBody, err := intercpt.Intercept(reqHeader, body, false)
   235  		if err != nil {
   236  			return err
   237  		}
   238  		r.Request = hdr
   239  		r.Body = io.NopCloser(bytes.NewReader(responseBody))
   240  		r.ContentLength = int64(len(responseBody))
   241  		r.Header.Set("Content-Length", strconv.Itoa(len(responseBody)))
   242  		return err
   243  	}
   244  	proxy.ServeHTTP(c.Writer, c.Request)
   245  }
   246  
   247  func filterEmergencyAccessURL(path string) bool {
   248  	re := regexp.MustCompile(emergencyAccessPathRegex)
   249  	return re.MatchString(path)
   250  }
   251  
   252  func validSession(session sessions.Session) bool {
   253  	expiresAt := session.Get(types.SessionExpirationField)
   254  	if expiresAt != nil {
   255  		expirationTime := expiresAt.(time.Time)
   256  		return !expirationTime.Before(time.Now().UTC()) && session.Get(types.SessionIDField) != nil
   257  	}
   258  	return false
   259  }
   260  
   261  func getAPIEndpoint(c *gin.Context, log logr.Logger, cfg *ProxyConfig) *url.URL {
   262  	cfgEndpoint := cfg.BffEndpoint
   263  	path := c.Request.URL.Path
   264  
   265  	if strings.HasPrefix(c.Request.URL.Path, "/api/ea/") {
   266  		cfgEndpoint = cfg.EAGatewayEndpoint
   267  		path = strings.TrimPrefix(path, "/api/")
   268  	}
   269  	apiEndpointString, err := url.JoinPath(cfgEndpoint, path)
   270  	if err != nil {
   271  		log.Error(err, "Error joining url and path for reverse proxy", "url", cfgEndpoint, "path", path)
   272  		return nil
   273  	}
   274  	apiEndpoint, err := url.Parse(apiEndpointString)
   275  	if err != nil {
   276  		log.Error(err, "Error parsing Edge API target URL for reverse proxy", "url", apiEndpoint, "path", path)
   277  		return nil
   278  	}
   279  	return apiEndpoint
   280  }
   281  

View as plain text